Bläddra i källkod

Merge branch 'master' into cylinders

Alexander Rose 4 år sedan
förälder
incheckning
dd1bca0fee
58 ändrade filer med 797 tillägg och 228 borttagningar
  1. 2 2
      package-lock.json
  2. 1 1
      package.json
  3. 43 0
      src/extensions/alpha-orbitals/transforms.ts
  4. 13 2
      src/mol-canvas3d/canvas3d.ts
  5. 1 1
      src/mol-canvas3d/helper/bounding-sphere-helper.ts
  6. 1 0
      src/mol-canvas3d/helper/camera-helper.ts
  7. 1 0
      src/mol-canvas3d/helper/handle-helper.ts
  8. 5 3
      src/mol-canvas3d/passes/multi-sample.ts
  9. 1 0
      src/mol-geo/geometry/base.ts
  10. 2 1
      src/mol-gl/_spec/renderer.spec.ts
  11. 16 11
      src/mol-gl/compute/histogram-pyramid/reduction.ts
  12. 5 3
      src/mol-gl/compute/marching-cubes/active-voxels.ts
  13. 5 9
      src/mol-gl/compute/marching-cubes/isosurface.ts
  14. 1 0
      src/mol-gl/renderable.ts
  15. 1 0
      src/mol-gl/renderable/util.ts
  16. 14 7
      src/mol-gl/renderer.ts
  17. 12 1
      src/mol-gl/shader/chunks/assign-material-color.glsl.ts
  18. 5 5
      src/mol-gl/shader/gaussian-density.frag.ts
  19. 2 3
      src/mol-gl/shader/gaussian-density.vert.ts
  20. 16 5
      src/mol-gl/shader/histogram-pyramid/reduction.frag.ts
  21. 1 2
      src/mol-gl/shader/marching-cubes/active-voxels.frag.ts
  22. 2 5
      src/mol-gl/shader/marching-cubes/isosurface.frag.ts
  23. 51 21
      src/mol-gl/webgl/compat.ts
  24. 3 3
      src/mol-gl/webgl/context.ts
  25. 22 2
      src/mol-gl/webgl/extensions.ts
  26. 7 5
      src/mol-gl/webgl/render-target.ts
  27. 28 1
      src/mol-gl/webgl/resources.ts
  28. 43 4
      src/mol-gl/webgl/texture.ts
  29. 1 1
      src/mol-math/geometry/gaussian-density.ts
  30. 37 23
      src/mol-math/geometry/gaussian-density/gpu.ts
  31. 3 3
      src/mol-model-formats/structure/basic/schema.ts
  32. 89 0
      src/mol-model-formats/structure/pdb/conect.ts
  33. 23 3
      src/mol-model-formats/structure/pdb/to-cif.ts
  34. 10 1
      src/mol-model-formats/structure/property/bonds/struct_conn.ts
  35. 3 2
      src/mol-model/structure/model/properties/common.ts
  36. 4 2
      src/mol-model/structure/model/types.ts
  37. 1 1
      src/mol-model/structure/structure/structure.ts
  38. 1 1
      src/mol-model/structure/structure/unit/bonds.ts
  39. 3 0
      src/mol-model/structure/structure/unit/bonds/inter-compute.ts
  40. 3 0
      src/mol-model/structure/structure/unit/bonds/intra-compute.ts
  41. 10 4
      src/mol-plugin-state/builder/structure/representation-preset.ts
  42. 2 2
      src/mol-plugin-state/helpers/structure-selection-query.ts
  43. 107 37
      src/mol-plugin-ui/sequence.tsx
  44. 2 2
      src/mol-plugin-ui/sequence/sequence.tsx
  45. 25 7
      src/mol-plugin-ui/skin/base/components/sequence.scss
  46. 21 2
      src/mol-repr/structure/complex-visual.ts
  47. 3 1
      src/mol-repr/structure/representation/gaussian-surface.ts
  48. 93 19
      src/mol-repr/structure/visual/gaussian-surface-mesh.ts
  49. 1 1
      src/mol-repr/structure/visual/util/common.ts
  50. 11 2
      src/mol-repr/structure/visual/util/gaussian.ts
  51. 3 0
      src/servers/model/CHANGELOG.md
  52. 2 1
      src/servers/model/server/api-web-multiple.ts
  53. 8 7
      src/servers/model/server/api-web.ts
  54. 14 4
      src/servers/model/server/api.ts
  55. 7 0
      src/servers/model/server/jobs.ts
  56. 3 2
      src/servers/model/utils/writer.ts
  57. 1 1
      src/servers/model/version.ts
  58. 2 2
      src/tests/browser/marching-cubes.ts

+ 2 - 2
package-lock.json

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

+ 1 - 1
package.json

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

+ 43 - 0
src/extensions/alpha-orbitals/transforms.ts

@@ -19,6 +19,7 @@ import { Theme } from '../../mol-theme/theme';
 import { VolumeRepresentation3DHelpers } from '../../mol-plugin-state/transforms/representation';
 import { AlphaOrbital, Basis, CubeGrid } from './data-model';
 import { createSphericalCollocationDensityGrid } from './density';
+import { Tensor } from '../../mol-math/linear-algebra';
 
 export class BasisAndOrbitals extends PluginStateObject.Create<{ basis: Basis, order: SphericalBasisOrder, orbitals: AlphaOrbital[] }>({ name: 'Basis', typeClass: 'Object' }) { }
 
@@ -49,9 +50,43 @@ const CreateOrbitalVolumeParamBase = {
             { atomCount: 25, spacing: 0.4 },
             { atomCount: 0, spacing: 0.35 },
         ]
+    }),
+    clampValues: PD.MappedStatic('off', {
+        off: PD.EmptyGroup(),
+        on: PD.Group({
+            sigma: PD.Numeric(8, { min: 1, max: 20 }, { description: 'Clamp values to range [sigma * negIsoValue, sigma * posIsoValue].' })
+        })
     })
 };
 
+function clampData(matrix: Tensor.Data, min: number, max: number) {
+    for (let i = 0, _i = matrix.length; i < _i; i++) {
+        const v = matrix[i];
+        if (v < min) matrix[i] = min;
+        else if (v > max) matrix[i] = max;
+    }
+}
+
+function clampGrid(data: CubeGrid, v: number) {
+    const grid = data.grid;
+    const min = (data.isovalues?.negative ?? data.grid.stats.min) * v;
+    const max = (data.isovalues?.positive ?? data.grid.stats.max) * v;
+
+    // clamp values for better direct volume resolution
+    // current implementation uses Byte array for values
+    // if this is not enough, update mol* to use float
+    // textures instead
+    if (grid.stats.min < min || grid.stats.max > max) {
+        clampData(data.grid.cells.data, min, max);
+        if (grid.stats.min < min) {
+            (grid.stats.min as number) = min;
+        }
+        if (grid.stats.max > max) {
+            (grid.stats.max as number) = max;
+        }
+    }
+}
+
 export const CreateOrbitalVolume = PluginStateTransform.BuiltIn({
     name: 'create-orbital-volume',
     display: 'Orbital Volume',
@@ -84,6 +119,10 @@ export const CreateOrbitalVolume = PluginStateTransform.BuiltIn({
                 _propertyData: Object.create(null),
             };
 
+            if (params.clampValues?.name === 'on') {
+                clampGrid(data, params.clampValues?.params?.sigma ?? 8);
+            }
+
             return new PluginStateObject.Volume.Data(volume, { label: 'Orbital Volume' });
         });
     }
@@ -112,6 +151,10 @@ export const CreateOrbitalDensityVolume = PluginStateTransform.BuiltIn({
                 _propertyData: Object.create(null),
             };
 
+            if (params.clampValues?.name === 'on') {
+                clampGrid(data, params.clampValues?.params?.sigma ?? 8);
+            }
+
             return new PluginStateObject.Volume.Data(volume, { label: 'Orbital Volume' });
         });
     }

+ 13 - 2
src/mol-canvas3d/canvas3d.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 Alexander Rose <alexander.rose@weirdbyte.de>
  * @author David Sehnal <david.sehnal@gmail.com>
@@ -47,7 +47,7 @@ export const Canvas3DParams = {
             on: PD.Group(StereoCameraParams),
             off: PD.Group({})
         }, { cycle: true, hideIf: p => p?.mode !== 'perspective' }),
-        manualReset: PD.Boolean(false, { isHidden: true })
+        manualReset: PD.Boolean(false, { isHidden: true }),
     }, { pivot: 'mode' }),
     cameraFog: PD.MappedStatic('on', {
         on: PD.Group({
@@ -117,6 +117,7 @@ interface Canvas3D {
 
     notifyDidDraw: boolean,
     readonly didDraw: BehaviorSubject<now.Timestamp>
+    readonly commited: BehaviorSubject<now.Timestamp>
     readonly reprCount: BehaviorSubject<number>
     readonly resized: BehaviorSubject<any>
 
@@ -218,6 +219,7 @@ namespace Canvas3D {
 
         let startTime = now();
         const didDraw = new BehaviorSubject<now.Timestamp>(0 as now.Timestamp);
+        const commited = new BehaviorSubject<now.Timestamp>(0 as now.Timestamp);
 
         const { gl, contextRestored } = webgl;
 
@@ -389,6 +391,7 @@ namespace Canvas3D {
                     draw(true);
                     forceDrawAfterAllCommited = false;
                 }
+                commited.next(now());
             }
         }
 
@@ -464,6 +467,13 @@ namespace Canvas3D {
                 materialId: r.materialId,
             })));
             console.log(webgl.stats);
+
+            const { texture, attribute, elements } = webgl.resources.getByteCounts();
+            console.log({
+                texture: `${(texture / 1024 / 1024).toFixed(3)} MiB`,
+                attribute: `${(attribute / 1024 / 1024).toFixed(3)} MiB`,
+                elements: `${(elements / 1024 / 1024).toFixed(3)} MiB`,
+            });
         }
 
         function add(repr: Representation.Any) {
@@ -614,6 +624,7 @@ namespace Canvas3D {
             get notifyDidDraw() { return notifyDidDraw; },
             set notifyDidDraw(v: boolean) { notifyDidDraw = v; },
             didDraw,
+            commited,
             reprCount,
             resized,
             setProps: (properties, doNotRequestDraw = false) => {

+ 1 - 1
src/mol-canvas3d/helper/bounding-sphere-helper.ts

@@ -160,5 +160,5 @@ const instanceMaterialId = getNextMaterialId();
 
 function createBoundingSphereRenderObject(mesh: Mesh, color: Color, materialId: number, transform?: TransformData) {
     const values = Mesh.Utils.createValuesSimple(mesh, { alpha: 0.1, doubleSided: false }, color, 1, transform);
-    return createRenderObject('mesh', values, { visible: true, alphaFactor: 1, pickable: false, colorOnly: false, opaque: false, writeDepth: false }, materialId);
+    return createRenderObject('mesh', values, { visible: true, alphaFactor: 1, pickable: false, colorOnly: false, opaque: false, writeDepth: false, noClip: false }, materialId);
 }

+ 1 - 0
src/mol-canvas3d/helper/camera-helper.ts

@@ -70,6 +70,7 @@ export class CameraHelper {
                     this.scene.clear();
                     const params = { ...props.axes.params, scale: props.axes.params.scale * this.webgl.pixelRatio };
                     this.renderObject = createAxesRenderObject(params);
+                    this.renderObject.state.noClip = true;
                     this.scene.add(this.renderObject);
                     this.scene.commit();
 

+ 1 - 0
src/mol-canvas3d/helper/handle-helper.ts

@@ -72,6 +72,7 @@ export class HandleHelper {
                     this.scene.clear();
                     const params = { ...props.handle.params, scale: props.handle.params.scale * this.webgl.pixelRatio };
                     this.renderObject = createHandleRenderObject(params);
+                    this.renderObject.state.noClip = true;
                     this.scene.add(this.renderObject);
                     this.scene.commit();
 

+ 5 - 3
src/mol-canvas3d/passes/multi-sample.ts

@@ -1,5 +1,5 @@
 /**
- * Copyright (c) 2019-2020 mol* contributors, licensed under MIT, See LICENSE file for more info.
+ * Copyright (c) 2019-2021 mol* contributors, licensed under MIT, See LICENSE file for more info.
  *
  * @author Alexander Rose <alexander.rose@weirdbyte.de>
  */
@@ -69,11 +69,13 @@ export class MultiSamplePass {
     private compose: ComposeRenderable
 
     constructor(private webgl: WebGLContext, private drawPass: DrawPass) {
-        const { colorBufferFloat, textureFloat } = webgl.extensions;
+        const { colorBufferFloat, textureFloat, colorBufferHalfFloat, textureHalfFloat } = webgl.extensions;
         const width = drawPass.colorTarget.getWidth();
         const height = drawPass.colorTarget.getHeight();
         this.colorTarget = webgl.createRenderTarget(width, height, false);
-        this.composeTarget = webgl.createRenderTarget(width, height, false, colorBufferFloat && textureFloat ? 'float32' : 'uint8');
+        const type = colorBufferHalfFloat && textureHalfFloat ? 'fp16' :
+            colorBufferFloat && textureFloat ? 'float32' : 'uint8';
+        this.composeTarget = webgl.createRenderTarget(width, height, false, type);
         this.holdTarget = webgl.createRenderTarget(width, height, false);
         this.compose = getComposeRenderable(webgl, drawPass.colorTarget.texture);
     }

+ 1 - 0
src/mol-geo/geometry/base.ts

@@ -81,6 +81,7 @@ export namespace BaseGeometry {
             colorOnly: false,
             opaque,
             writeDepth: opaque,
+            noClip: false,
         };
     }
 

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

@@ -94,7 +94,8 @@ function createPoints() {
         pickable: true,
         colorOnly: false,
         opaque: true,
-        writeDepth: true
+        writeDepth: true,
+        noClip: false,
     };
 
     return createRenderObject('points', values, state, -1);

+ 16 - 11
src/mol-gl/compute/histogram-pyramid/reduction.ts

@@ -1,5 +1,5 @@
 /**
- * Copyright (c) 2019-2020 mol* contributors, licensed under MIT, See LICENSE file for more info.
+ * Copyright (c) 2019-2021 mol* contributors, licensed under MIT, See LICENSE file for more info.
  *
  * @author Alexander Rose <alexander.rose@weirdbyte.de>
  */
@@ -12,7 +12,7 @@ import { Texture, TextureFilter, TextureFormat, TextureKind, TextureType } from
 import { ShaderCode } from '../../../mol-gl/shader-code';
 import { ValueCell } from '../../../mol-util';
 import { QuadSchema, QuadValues } from '../util';
-import { Vec2 } from '../../../mol-math/linear-algebra';
+import { Vec2, Vec3 } from '../../../mol-math/linear-algebra';
 import { getHistopyramidSum } from './sum';
 import { Framebuffer } from '../../../mol-gl/webgl/framebuffer';
 import { isPowerOfTwo } from '../../../mol-math/misc';
@@ -24,6 +24,7 @@ const HistopyramidReductionSchema = {
     tPreviousLevel: TextureSpec('texture', 'rgba', 'float', 'nearest'),
     uSize: UniformSpec('f'),
     uTexSize: UniformSpec('f'),
+    uFirst: UniformSpec('b'),
 };
 
 const HistogramPyramidName = 'histogram-pyramid';
@@ -47,6 +48,7 @@ function createHistopyramidReductionRenderable(ctx: WebGLContext, initialTexture
         tPreviousLevel: ValueCell.create(initialTexture),
         uSize: ValueCell.create(0),
         uTexSize: ValueCell.create(0),
+        uFirst: ValueCell.create(true),
     };
 
     const schema = { ...HistopyramidReductionSchema };
@@ -77,7 +79,7 @@ function setRenderingDefaults(ctx: WebGLContext) {
     state.disable(gl.CULL_FACE);
     state.disable(gl.BLEND);
     state.disable(gl.DEPTH_TEST);
-    state.disable(gl.SCISSOR_TEST);
+    state.enable(gl.SCISSOR_TEST);
     state.depthMask(false);
     state.colorMask(true, true, true, true);
     state.clearColor(0, 0, 0, 0);
@@ -107,18 +109,20 @@ export interface HistogramPyramid {
     scale: Vec2
 }
 
-export function createHistogramPyramid(ctx: WebGLContext, inputTexture: Texture, scale: Vec2): HistogramPyramid {
+export function createHistogramPyramid(ctx: WebGLContext, inputTexture: Texture, scale: Vec2, gridTexDim: Vec3): HistogramPyramid {
     const { gl } = ctx;
+    const w = inputTexture.getWidth();
+    const h = inputTexture.getHeight();
 
     // printTexture(ctx, inputTexture, 2)
-    if (inputTexture.getWidth() !== inputTexture.getHeight() || !isPowerOfTwo(inputTexture.getWidth())) {
+    if (w !== h || !isPowerOfTwo(w)) {
         throw new Error('inputTexture must be of square power-of-two size');
     }
 
     // This part set the levels
-    const levels = Math.ceil(Math.log(inputTexture.getWidth()) / Math.log(2));
+    const levels = Math.ceil(Math.log(w) / Math.log(2));
     const maxSize = Math.pow(2, levels);
-    // console.log('levels', levels, 'maxSize', maxSize, 'input', inputTexture.getWidth());
+    // console.log('levels', levels, 'maxSize', maxSize, 'input', w);
 
     const pyramidTexture = getTexture('pyramid', ctx, 'image-float32', 'rgba', 'float', 'nearest');
     pyramidTexture.define(maxSize, maxSize);
@@ -140,20 +144,22 @@ export function createHistogramPyramid(ctx: WebGLContext, inputTexture: Texture,
         const currLevel = levels - 1 - i;
         const tf = levelTexturesFramebuffers[currLevel];
         tf.framebuffer.bind();
-        // levelTextures[currLevel].attachFramebuffer(framebuffer, 0)
 
         const size = Math.pow(2, currLevel);
         // console.log('size', size, 'draw-level', currLevel, 'read-level', levels - i)
 
         ValueCell.update(renderable.values.uSize, Math.pow(2, i + 1) / maxSize);
         ValueCell.update(renderable.values.uTexSize, size);
+        ValueCell.updateIfChanged(renderable.values.uFirst, i === 0);
         if (i > 0) {
             ValueCell.update(renderable.values.tPreviousLevel, levelTexturesFramebuffers[levels - i].texture);
             renderable.update();
         }
         ctx.state.currentRenderItemId = -1;
         gl.viewport(0, 0, size, size);
+        gl.scissor(0, 0, size, size);
         gl.clear(gl.COLOR_BUFFER_BIT);
+        gl.scissor(0, 0, gridTexDim[0], gridTexDim[1]);
         renderable.render();
 
         pyramidTexture.bind(0);
@@ -171,9 +177,8 @@ export function createHistogramPyramid(ctx: WebGLContext, inputTexture: Texture,
 
     const finalCount = getHistopyramidSum(ctx, levelTexturesFramebuffers[0].texture);
     const height = Math.ceil(finalCount / Math.pow(2, levels));
-    // const scale = Vec2.create(maxSize / inputTexture.width, maxSize / inputTexture.height)
-    // console.log('height', height, 'finalCount', finalCount, 'scale', scale)
-
+    // const scale = Vec2.create(maxSize / inputTexture.width, maxSize / inputTexture.height);
+    // console.log('height', height, 'finalCount', finalCount, 'scale', scale);
 
     return {
         pyramidTex: pyramidTexture,

+ 5 - 3
src/mol-gl/compute/marching-cubes/active-voxels.ts

@@ -1,5 +1,5 @@
 /**
- * Copyright (c) 2019 mol* contributors, licensed under MIT, See LICENSE file for more info.
+ * Copyright (c) 2019-2021 mol* contributors, licensed under MIT, See LICENSE file for more info.
  *
  * @author Alexander Rose <alexander.rose@weirdbyte.de>
  */
@@ -75,7 +75,7 @@ function setRenderingDefaults(ctx: WebGLContext) {
     state.disable(gl.CULL_FACE);
     state.disable(gl.BLEND);
     state.disable(gl.DEPTH_TEST);
-    state.disable(gl.SCISSOR_TEST);
+    state.enable(gl.SCISSOR_TEST);
     state.depthMask(false);
     state.colorMask(true, true, true, true);
     state.clearColor(0, 0, 0, 0);
@@ -93,7 +93,7 @@ export function calcActiveVoxels(ctx: WebGLContext, volumeData: Texture, gridDim
     framebuffer.bind();
 
     if (!ctx.namedTextures[ActiveVoxelsName]) {
-        ctx.namedTextures[ActiveVoxelsName] = resources.texture('image-float32', 'rgba', 'float', 'nearest');
+        ctx.namedTextures[ActiveVoxelsName] = resources.texture('image-uint8', 'rgba', 'ubyte', 'nearest');
     }
     const activeVoxelsTex = ctx.namedTextures[ActiveVoxelsName];
     activeVoxelsTex.define(width, height);
@@ -104,7 +104,9 @@ export function calcActiveVoxels(ctx: WebGLContext, volumeData: Texture, gridDim
     activeVoxelsTex.attachFramebuffer(framebuffer, 0);
     setRenderingDefaults(ctx);
     gl.viewport(0, 0, width, height);
+    gl.scissor(0, 0, width, height);
     gl.clear(gl.COLOR_BUFFER_BIT);
+    gl.scissor(0, 0, gridTexDim[0], gridTexDim[1]);
     renderable.render();
 
     // console.log('gridScale', gridScale, 'gridTexDim', gridTexDim, 'gridDim', gridDim)

+ 5 - 9
src/mol-gl/compute/marching-cubes/isosurface.ts

@@ -1,5 +1,5 @@
 /**
- * Copyright (c) 2019 mol* contributors, licensed under MIT, See LICENSE file for more info.
+ * Copyright (c) 2019-2021 mol* contributors, licensed under MIT, See LICENSE file for more info.
  *
  * @author Alexander Rose <alexander.rose@weirdbyte.de>
  */
@@ -44,7 +44,6 @@ function getIsosurfaceRenderable(ctx: WebGLContext, activeVoxelsPyramid: Texture
     if (ctx.namedComputeRenderables[IsosurfaceName]) {
         const v = ctx.namedComputeRenderables[IsosurfaceName].values;
 
-        ValueCell.update(v.uQuadScale, Vec2.create(1, height / Math.pow(2, levels)));
         ValueCell.update(v.tActiveVoxelsPyramid, activeVoxelsPyramid);
         ValueCell.update(v.tActiveVoxelsBase, activeVoxelsBase);
         ValueCell.update(v.tVolumeData, volumeData);
@@ -72,7 +71,6 @@ function createIsosurfaceRenderable(ctx: WebGLContext, activeVoxelsPyramid: Text
         ...QuadValues,
         tTriIndices: ValueCell.create(getTriIndices()),
 
-        uQuadScale: ValueCell.create(Vec2.create(1, height / Math.pow(2, levels))),
         tActiveVoxelsPyramid: ValueCell.create(activeVoxelsPyramid),
         tActiveVoxelsBase: ValueCell.create(activeVoxelsBase),
         tVolumeData: ValueCell.create(volumeData),
@@ -109,6 +107,7 @@ function setRenderingDefaults(ctx: WebGLContext) {
 export function createIsosurfaceBuffers(ctx: WebGLContext, activeVoxelsBase: Texture, volumeData: Texture, histogramPyramid: HistogramPyramid, gridDim: Vec3, gridTexDim: Vec3, transform: Mat4, isoValue: number, vertexGroupTexture?: Texture, normalTexture?: Texture) {
     const { gl, resources } = ctx;
     const { pyramidTex, height, levels, scale, count } = histogramPyramid;
+    const width = pyramidTex.getWidth();
 
     // console.log('iso', 'gridDim', gridDim, 'scale', scale, 'gridTexDim', gridTexDim)
     // console.log('iso volumeData', volumeData)
@@ -118,18 +117,15 @@ export function createIsosurfaceBuffers(ctx: WebGLContext, activeVoxelsBase: Tex
     }
     const framebuffer = ctx.namedFramebuffers[IsosurfaceName];
 
-    const w = pyramidTex.getWidth();
-    const h = pyramidTex.getHeight();
-
     if (!vertexGroupTexture) {
         vertexGroupTexture = resources.texture('image-float32', 'rgba', 'float', 'nearest');
     }
-    vertexGroupTexture.define(w, h);
+    vertexGroupTexture.define(width, height);
 
     if (!normalTexture) {
         normalTexture = resources.texture('image-float32', 'rgba', 'float', 'nearest');
     }
-    normalTexture.define(w, h);
+    normalTexture.define(width, height);
 
     // const infoTex = createTexture(ctx, 'image-float32', 'rgba', 'float', 'nearest')
     // infoTex.define(pyramidTex.width, pyramidTex.height)
@@ -170,7 +166,7 @@ export function createIsosurfaceBuffers(ctx: WebGLContext, activeVoxelsBase: Tex
     ]);
 
     setRenderingDefaults(ctx);
-    gl.viewport(0, 0, w, h);
+    gl.viewport(0, 0, width, height);
     gl.clear(gl.COLOR_BUFFER_BIT);
     renderable.render();
 

+ 1 - 0
src/mol-gl/renderable.ts

@@ -21,6 +21,7 @@ export type RenderableState = {
     colorOnly: boolean
     opaque: boolean
     writeDepth: boolean
+    noClip: boolean
 }
 
 export interface Renderable<T extends RenderableValues> {

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

@@ -76,6 +76,7 @@ export function printImageData(imageData: ImageData, scale = 1, pixelated = fals
         img.style.top = '0px';
         img.style.left = '0px';
         img.style.border = 'solid grey';
+        img.style.pointerEvents = 'none';
         document.body.appendChild(img);
     }, 'image/png');
 }

+ 14 - 7
src/mol-gl/renderer.ts

@@ -268,13 +268,20 @@ namespace Renderer {
             }
 
             let definesNeedUpdate = false;
-            if (r.values.dClipObjectCount.ref.value !== clip.objects.count) {
-                ValueCell.update(r.values.dClipObjectCount, clip.objects.count);
-                definesNeedUpdate = true;
-            }
-            if (r.values.dClipVariant.ref.value !== clip.variant) {
-                ValueCell.update(r.values.dClipVariant, clip.variant);
-                definesNeedUpdate = true;
+            if (r.state.noClip) {
+                if (r.values.dClipObjectCount.ref.value !== 0) {
+                    ValueCell.update(r.values.dClipObjectCount, 0);
+                    definesNeedUpdate = true;
+                }
+            } else {
+                if (r.values.dClipObjectCount.ref.value !== clip.objects.count) {
+                    ValueCell.update(r.values.dClipObjectCount, clip.objects.count);
+                    definesNeedUpdate = true;
+                }
+                if (r.values.dClipVariant.ref.value !== clip.variant) {
+                    ValueCell.update(r.values.dClipVariant, clip.variant);
+                    definesNeedUpdate = true;
+                }
             }
             if (definesNeedUpdate) r.update();
 

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

@@ -40,7 +40,18 @@ export default `
                 4.0 / 17.0, 12.0 / 17.0,  2.0 / 17.0, 10.0 / 17.0,
                 16.0 / 17.0,  8.0 / 17.0, 14.0 / 17.0,  6.0 / 17.0
             );
-            at = thresholdMatrix[int(intMod(coord.x, 4.0))][int(intMod(coord.y, 4.0))];
+            int ci = int(intMod(coord.x, 4.0));
+            int ri = int(intMod(coord.y, 4.0));
+            #if __VERSION__ == 100
+                vec4 i = vec4(float(ci * 4 + ri));
+                vec4 v = thresholdMatrix[0] * vec4(equal(i, vec4(0.0, 1.0, 2.0, 3.0))) +
+                    thresholdMatrix[1] * vec4(equal(i, vec4(4.0, 5.0, 6.0, 7.0))) +
+                    thresholdMatrix[2] * vec4(equal(i, vec4(8.0, 9.0, 10.0, 11.0))) +
+                    thresholdMatrix[3] * vec4(equal(i, vec4(12.0, 13.0, 14.0, 15.0)));
+                at = v.x + v.y + v.z + v.w;
+            #else
+                at = thresholdMatrix[ci][ri];
+            #endif
 
             if (ta < 0.99 && (ta < 0.01 || ta < at)) {
                 discard;

+ 5 - 5
src/mol-gl/shader/gaussian-density.frag.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 Alexander Rose <alexander.rose@weirdbyte.de>
  * @author Michael Krone <michael.krone@uni-tuebingen.de>
@@ -31,7 +31,7 @@ uniform float uCurrentX;
 uniform float uCurrentY;
 uniform float uAlpha;
 uniform float uResolution;
-uniform float uRadiusFactor;
+uniform float uRadiusFactorInv;
 
 void main() {
     vec2 v = gl_FragCoord.xy - vec2(uCurrentX, uCurrentY) - 0.5;
@@ -40,16 +40,16 @@ void main() {
 
     #if defined(dCalcType_density)
         float density = exp(-uAlpha * ((dist * dist) * vRadiusSqInv));
-        gl_FragColor.a = density / uRadiusFactor;
+        gl_FragColor.a = density * uRadiusFactorInv;
     #elif defined(dCalcType_minDistance)
-        gl_FragColor.a = 1.0 - dist / uRadiusFactor;
+        gl_FragColor.a = 1.0 - dist * uRadiusFactorInv;
     #elif defined(dCalcType_groupId)
         #if defined(dGridTexType_2d)
             float minDistance = 1.0 - texture2D(tMinDistanceTex, (gl_FragCoord.xy) / (uGridTexDim.xy / uGridTexScale)).a;
         #elif defined(dGridTexType_3d)
             float minDistance = 1.0 - texelFetch(tMinDistanceTex, ivec3(gl_FragCoord.xy, uCurrentSlice), 0).a;
         #endif
-        if (dist / uRadiusFactor > minDistance + uResolution * 0.05)
+        if (dist * uRadiusFactorInv > minDistance + uResolution * 0.05)
             discard;
         gl_FragColor.rgb = encodeFloatRGB(vGroup);
     #endif

+ 2 - 3
src/mol-gl/shader/gaussian-density.vert.ts

@@ -1,5 +1,5 @@
 /**
- * Copyright (c) 2018-2019 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 Alexander Rose <alexander.rose@weirdbyte.de>
  * @author Michael Krone <michael.krone@uni-tuebingen.de>
@@ -21,7 +21,6 @@ varying float vRadiusSqInv;
 
 uniform vec3 uBboxSize;
 uniform vec3 uBboxMin;
-uniform float uCurrentSlice;
 uniform float uResolution;
 
 void main() {
@@ -29,7 +28,7 @@ void main() {
     #if defined(dCalcType_groupId)
         vGroup = aGroup;
     #endif
-    gl_PointSize = floor(((aRadius * 6.0) / uResolution) + 0.5);
+    gl_PointSize = ceil(((aRadius * 3.0) / uResolution) + uResolution);
     vPosition = (aPosition - uBboxMin) / uResolution;
     gl_Position = vec4(((aPosition - uBboxMin) / uBboxSize) * 2.0 - 1.0, 1.0);
 }

+ 16 - 5
src/mol-gl/shader/histogram-pyramid/reduction.frag.ts

@@ -5,17 +5,28 @@ precision highp sampler2D;
 // input texture (previous level used to evaluate the new level)
 uniform sampler2D tPreviousLevel;
 
-// 1/size of the previous level texture.
+// inverted size of the previous level texture.
 uniform float uSize;
 uniform float uTexSize;
+uniform bool uFirst;
 
 void main(void) {
     float k = 0.5 * uSize;
     vec2 position = floor((gl_FragCoord.xy / uTexSize) / uSize) * uSize;
-    float a = texture2D(tPreviousLevel, position).r;
-    float b = texture2D(tPreviousLevel, position + vec2(k, 0.)).r;
-    float c = texture2D(tPreviousLevel, position + vec2(0., k)).r;
-    float d = texture2D(tPreviousLevel, position + vec2(k, k)).r;
+    float a, b, c, d;
+
+    if (uFirst) {
+        a = texture2D(tPreviousLevel, position).r * 255.0;
+        b = texture2D(tPreviousLevel, position + vec2(k, 0.0)).r * 255.0;
+        c = texture2D(tPreviousLevel, position + vec2(0.0, k)).r * 255.0;
+        d = texture2D(tPreviousLevel, position + vec2(k, k)).r * 255.0;
+    } else {
+        a = texture2D(tPreviousLevel, position).r;
+        b = texture2D(tPreviousLevel, position + vec2(k, 0.0)).r;
+        c = texture2D(tPreviousLevel, position + vec2(0.0, k)).r;
+        d = texture2D(tPreviousLevel, position + vec2(k, k)).r;
+    }
+
     gl_FragColor.a = a;
     gl_FragColor.b = a + b;
     gl_FragColor.g = gl_FragColor.b + c;

+ 1 - 2
src/mol-gl/shader/marching-cubes/active-voxels.frag.ts

@@ -37,7 +37,6 @@ vec4 texture3dFrom2dNearest(sampler2D tex, vec3 pos, vec3 gridDim, vec2 texDim)
     float column = intMod(zSlice * gridDim.x, texDim.x) / gridDim.x;
     float row = floor(intDiv(zSlice * gridDim.x, texDim.x));
     vec2 coord = (vec2(column * gridDim.x, row * gridDim.y) + (pos.xy * gridDim.xy)) / (texDim / uScale);
-    // return texture2D(tex, coord + 0.5 / texDim);
     return texture2D(tex, coord);
 }
 
@@ -62,7 +61,7 @@ void main(void) {
 
     // get total triangles to generate for calculated MC case from triCount texture
     float totalTrianglesToGenerate = texture2D(tTriCount, vec2(intMod(c, 16.), floor(c / 16.)) / 16.).a;
-    gl_FragColor = vec4(vec3(floor(totalTrianglesToGenerate * 255.0 + 0.5) * 3.0), c);
+    gl_FragColor = vec4(vec3(totalTrianglesToGenerate * 3.0), c / 255.0);
 
     // gl_FragColor = vec4(255.0, 0.0, 0.0, voxel(posXYZ + c4 / uGridDim).a * 255.0);
     // gl_FragColor = vec4(255.0, 0.0, 0.0, voxel(posXYZ).a * 255.0);

+ 2 - 5
src/mol-gl/shader/marching-cubes/isosurface.frag.ts

@@ -19,8 +19,6 @@ uniform mat4 uGridTransform;
 // scale to volume data coord
 uniform vec2 uScale;
 
-// varying vec2 vCoordinate;
-
 #include common
 
 // cube corners
@@ -50,7 +48,6 @@ vec4 texture3dFrom2dNearest(sampler2D tex, vec3 pos, vec3 gridDim, vec2 texDim)
     float row = floor(intDiv(zSlice * gridDim.x, texDim.x));
     vec2 coord = (vec2(column * gridDim.x, row * gridDim.y) + (pos.xy * gridDim.xy)) / (texDim / uScale);
     return texture2D(tex, coord + 0.5 / (texDim / uScale));
-    // return texture2D(tex, coord);
 }
 
 vec4 voxel(vec3 pos) {
@@ -78,7 +75,7 @@ void main(void) {
     vec4 vI4 = vec4(vI);
 
     // traverse the different levels of the pyramid
-    for(int i = 1; i < 12; i++) {
+    for(int i = 1; i < 14; i++) {
         if(float(i) >= uLevels) break;
 
         offset -= diff;
@@ -103,7 +100,7 @@ void main(void) {
     vec2 coord2d = position / uScale;
     vec3 coord3d = floor(index3dFrom2d(coord2d) + 0.5);
 
-    float edgeIndex = floor(texture2D(tActiveVoxelsBase, position).a + 0.5);
+    float edgeIndex = floor(texture2D(tActiveVoxelsBase, position).a * 255.0 + 0.5);
 
     // current vertex for the up to 15 MC cases
     float currentVertex = vI - dot(m, starts);

+ 51 - 21
src/mol-gl/webgl/compat.ts

@@ -1,11 +1,11 @@
-import { isDebugMode } from '../../mol-util/debug';
 /**
- * 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 Alexander Rose <alexander.rose@weirdbyte.de>
  */
 
-import { getErrorDescription, getGLContext } from './context';
+import { isDebugMode } from '../../mol-util/debug';
+import { getErrorDescription } from './context';
 import { getProgram } from './program';
 import { getShader } from './shader';
 
@@ -111,6 +111,27 @@ export function getTextureFloatLinear(gl: GLRenderingContext): COMPAT_texture_fl
     return gl.getExtension('OES_texture_float_linear');
 }
 
+export interface COMPAT_texture_half_float {
+    readonly HALF_FLOAT: number
+}
+
+export function getTextureHalfFloat(gl: GLRenderingContext): COMPAT_texture_half_float | null {
+    if (isWebGL2(gl)) {
+        return { HALF_FLOAT: gl.HALF_FLOAT };
+    } else {
+        const ext = gl.getExtension('OES_texture_half_float');
+        if (ext === null) return null;
+        return { HALF_FLOAT: ext.HALF_FLOAT_OES };
+    }
+}
+
+export interface COMPAT_texture_half_float_linear {
+}
+
+export function getTextureHalfFloatLinear(gl: GLRenderingContext): COMPAT_texture_half_float_linear | null {
+    return gl.getExtension('OES_texture_half_float_linear');
+}
+
 export interface COMPAT_blend_minmax {
     readonly MIN: number
     readonly MAX: number
@@ -146,13 +167,35 @@ export function getColorBufferFloat(gl: GLRenderingContext): COMPAT_color_buffer
         const ext = gl.getExtension('WEBGL_color_buffer_float');
         if (ext === null) {
             // test as support may not be advertised by browsers
-            return testColorBufferFloat() ? { RGBA32F: 0x8814 } : null;
+            gl.getExtension('OES_texture_float');
+            return testColorBuffer(gl, gl.FLOAT) ? { RGBA32F: 0x8814 } : null;
         }
         gl.getExtension('EXT_float_blend');
         return { RGBA32F: ext.RGBA32F_EXT };
     }
 }
 
+export interface COMPAT_color_buffer_half_float {
+    readonly RGBA16F: number;
+}
+
+export function getColorBufferHalfFloat(gl: GLRenderingContext): COMPAT_color_buffer_half_float | null {
+    if (isWebGL2(gl)) {
+        if (gl.getExtension('EXT_color_buffer_half_float') === null) return null;
+        gl.getExtension('EXT_float_blend');
+        return { RGBA16F: gl.RGBA16F };
+    } else {
+        const ext = gl.getExtension('EXT_color_buffer_half_float');
+        if (ext === null) {
+            // test as support may not be advertised by browsers
+            gl.getExtension('OES_texture_half_float');
+            return testColorBuffer(gl, 0x8D61) ? { RGBA16F: 0x881A } : null;
+        }
+        gl.getExtension('EXT_float_blend');
+        return { RGBA16F: ext.RGBA16F_EXT };
+    }
+}
+
 export interface COMPAT_draw_buffers {
     drawBuffers(buffers: number[]): void;
     readonly COLOR_ATTACHMENT0: number;
@@ -283,7 +326,7 @@ const TextureTestVertShader = `
 attribute vec4 aPosition;
 
 void main() {
-  gl_Position = aPosition;
+    gl_Position = aPosition;
 }`;
 
 const TextureTestFragShader = `
@@ -292,28 +335,15 @@ uniform vec4 uColor;
 uniform sampler2D uTexture;
 
 void main() {
-  gl_FragColor = texture2D(uTexture, vec2(0.5, 0.5)) * uColor;
+    gl_FragColor = texture2D(uTexture, vec2(0.5, 0.5)) * uColor;
 }`;
 
 const TextureTestTexCoords = new Float32Array([
     -1.0, -1.0, 1.0, -1.0, -1.0,  1.0, -1.0,  1.0, 1.0, -1.0, 1.0,  1.0
 ]);
 
-export function testColorBufferFloat() {
-    // adapted from https://stackoverflow.com/questions/28827511/
-
-    // Get A WebGL context
-    const canvas = document.createElement('canvas');
-    canvas.width = 16;
-    canvas.height = 16;
-    canvas.style.width = `${16}px`;
-    canvas.style.height = `${16}px`;
-    const gl = getGLContext(canvas);
-    if (gl === null) throw new Error('Unable to get WebGL context');
-
-    const type = gl.FLOAT;
-    gl.getExtension('OES_texture_float');
-
+// adapted from https://stackoverflow.com/questions/28827511/
+export function testColorBuffer(gl: GLRenderingContext, type: number) {
     // setup shaders
     const vertShader = getShader(gl, { type: 'vert', source: TextureTestVertShader });
     const fragShader = getShader(gl, { type: 'frag', source: TextureTestFragShader });

+ 3 - 3
src/mol-gl/webgl/context.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 Alexander Rose <alexander.rose@weirdbyte.de>
  */
@@ -202,7 +202,7 @@ export interface WebGLContext {
     /** Cache for textures, managed by consumers */
     readonly namedTextures: { [name: string]: Texture }
 
-    createRenderTarget: (width: number, height: number, depth?: boolean, type?: 'uint8' | 'float32', filter?: TextureFilter) => RenderTarget
+    createRenderTarget: (width: number, height: number, depth?: boolean, type?: 'uint8' | 'float32' | 'fp16', filter?: TextureFilter) => RenderTarget
     unbindFramebuffer: () => void
     readPixels: (x: number, y: number, width: number, height: number, buffer: Uint8Array | Float32Array) => void
     readPixelsAsync: (x: number, y: number, width: number, height: number, buffer: Uint8Array) => Promise<void>
@@ -317,7 +317,7 @@ export function createContext(gl: GLRenderingContext, props: Partial<{ pixelScal
             contextRestored.next(now());
         },
 
-        createRenderTarget: (width: number, height: number, depth?: boolean, type?: 'uint8' | 'float32', filter?: TextureFilter) => {
+        createRenderTarget: (width: number, height: number, depth?: boolean, type?: 'uint8' | 'float32' | 'fp16', filter?: TextureFilter) => {
             const renderTarget = createRenderTarget(gl, resources, width, height, depth, type, filter);
             renderTargets.add(renderTarget);
             return {

+ 22 - 2
src/mol-gl/webgl/extensions.ts

@@ -1,10 +1,10 @@
 /**
- * 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 Alexander Rose <alexander.rose@weirdbyte.de>
  */
 
-import { GLRenderingContext, COMPAT_instanced_arrays, COMPAT_standard_derivatives, COMPAT_vertex_array_object, getInstancedArrays, getStandardDerivatives, getVertexArrayObject, COMPAT_element_index_uint, getElementIndexUint, COMPAT_texture_float, getTextureFloat, COMPAT_texture_float_linear, getTextureFloatLinear, COMPAT_blend_minmax, getBlendMinMax, getFragDepth, COMPAT_frag_depth, COMPAT_color_buffer_float, getColorBufferFloat, COMPAT_draw_buffers, getDrawBuffers, getShaderTextureLod, COMPAT_shader_texture_lod, getDepthTexture, COMPAT_depth_texture, COMPAT_sRGB, getSRGB } from './compat';
+import { GLRenderingContext, COMPAT_instanced_arrays, COMPAT_standard_derivatives, COMPAT_vertex_array_object, getInstancedArrays, getStandardDerivatives, getVertexArrayObject, COMPAT_element_index_uint, getElementIndexUint, COMPAT_texture_float, getTextureFloat, COMPAT_texture_float_linear, getTextureFloatLinear, COMPAT_blend_minmax, getBlendMinMax, getFragDepth, COMPAT_frag_depth, COMPAT_color_buffer_float, getColorBufferFloat, COMPAT_draw_buffers, getDrawBuffers, getShaderTextureLod, COMPAT_shader_texture_lod, getDepthTexture, COMPAT_depth_texture, COMPAT_sRGB, getSRGB, getTextureHalfFloat, getTextureHalfFloatLinear, COMPAT_texture_half_float, COMPAT_texture_half_float_linear, COMPAT_color_buffer_half_float, getColorBufferHalfFloat } from './compat';
 import { isDebugMode } from '../../mol-util/debug';
 
 export type WebGLExtensions = {
@@ -14,11 +14,14 @@ export type WebGLExtensions = {
     standardDerivatives: COMPAT_standard_derivatives | null
     textureFloat: COMPAT_texture_float | null
     textureFloatLinear: COMPAT_texture_float_linear | null
+    textureHalfFloat: COMPAT_texture_half_float | null
+    textureHalfFloatLinear: COMPAT_texture_half_float_linear | null
     depthTexture: COMPAT_depth_texture | null
     blendMinMax: COMPAT_blend_minmax | null
     vertexArrayObject: COMPAT_vertex_array_object | null
     fragDepth: COMPAT_frag_depth | null
     colorBufferFloat: COMPAT_color_buffer_float | null
+    colorBufferHalfFloat: COMPAT_color_buffer_half_float | null
     drawBuffers: COMPAT_draw_buffers | null
     shaderTextureLod: COMPAT_shader_texture_lod | null
     sRGB: COMPAT_sRGB | null
@@ -50,6 +53,16 @@ export function createExtensions(gl: GLRenderingContext): WebGLExtensions {
         // - can't be a required extension because it is not supported by `headless-gl`
         console.log('Could not find support for "texture_float_linear"');
     }
+    const textureHalfFloat = getTextureHalfFloat(gl);
+    if (isDebugMode && textureHalfFloat === null) {
+        console.log('Could not find support for "texture_half_float"');
+    }
+    const textureHalfFloatLinear = getTextureHalfFloatLinear(gl);
+    if (isDebugMode && textureHalfFloatLinear === null) {
+        // TODO handle non-support downstream (no gpu gaussian calc, no gpu mc???)
+        // - can't be a required extension because it is not supported by `headless-gl`
+        console.log('Could not find support for "texture_half_float_linear"');
+    }
     const depthTexture = getDepthTexture(gl);
     if (isDebugMode && depthTexture === null) {
         console.log('Could not find support for "depth_texture"');
@@ -72,6 +85,10 @@ export function createExtensions(gl: GLRenderingContext): WebGLExtensions {
     if (isDebugMode && colorBufferFloat === null) {
         console.log('Could not find support for "color_buffer_float"');
     }
+    const colorBufferHalfFloat = getColorBufferHalfFloat(gl);
+    if (isDebugMode && colorBufferHalfFloat === null) {
+        console.log('Could not find support for "color_buffer_half_float"');
+    }
     const drawBuffers = getDrawBuffers(gl);
     if (isDebugMode && drawBuffers === null) {
         console.log('Could not find support for "draw_buffers"');
@@ -90,6 +107,8 @@ export function createExtensions(gl: GLRenderingContext): WebGLExtensions {
         standardDerivatives,
         textureFloat,
         textureFloatLinear,
+        textureHalfFloat,
+        textureHalfFloatLinear,
         elementIndexUint,
         depthTexture,
 
@@ -97,6 +116,7 @@ export function createExtensions(gl: GLRenderingContext): WebGLExtensions {
         vertexArrayObject,
         fragDepth,
         colorBufferFloat,
+        colorBufferHalfFloat,
         drawBuffers,
         shaderTextureLod,
         sRGB,

+ 7 - 5
src/mol-gl/webgl/render-target.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 Alexander Rose <alexander.rose@weirdbyte.de>
  */
@@ -26,12 +26,14 @@ export interface RenderTarget {
     destroy: () => void
 }
 
-export function createRenderTarget(gl: GLRenderingContext, resources: WebGLResources, _width: number, _height: number, depth = true, type: 'uint8' | 'float32' = 'uint8', filter: TextureFilter = 'nearest'): RenderTarget {
+export function createRenderTarget(gl: GLRenderingContext, resources: WebGLResources, _width: number, _height: number, depth = true, type: 'uint8' | 'float32' | 'fp16' = 'uint8', filter: TextureFilter = 'nearest'): RenderTarget {
 
     const framebuffer = resources.framebuffer();
-    const targetTexture = type === 'float32'
-        ? resources.texture('image-float32', 'rgba', 'float', filter)
-        : resources.texture('image-uint8', 'rgba', 'ubyte', filter);
+    const targetTexture = type === 'fp16'
+        ? resources.texture('image-float16', 'rgba', 'fp16', filter)
+        : type === 'float32'
+            ? resources.texture('image-float32', 'rgba', 'float', filter)
+            : resources.texture('image-uint8', 'rgba', 'ubyte', filter);
     // make a depth renderbuffer of the same size as the targetTexture
     const depthRenderbuffer = depth
         ? resources.renderbuffer('depth16', 'depth', _width, _height)

+ 28 - 1
src/mol-gl/webgl/resources.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>
  */
@@ -45,6 +45,12 @@ interface Resource {
 
 type ResourceName = keyof WebGLStats['resourceCounts']
 
+type ByteCounts = {
+    texture: number
+    attribute: number
+    elements: number
+}
+
 export interface WebGLResources {
     attribute: (array: ArrayType, itemSize: AttributeItemSize, divisor: number, usageHint?: UsageHint) => AttributeBuffer
     elements: (array: ElementsType, usageHint?: UsageHint) => ElementsBuffer
@@ -55,6 +61,8 @@ export interface WebGLResources {
     texture: (kind: TextureKind, format: TextureFormat, type: TextureType, filter: TextureFilter) => Texture,
     vertexArray: (program: Program, attributeBuffers: AttributeBuffers, elementsBuffer?: ElementsBuffer) => VertexArray,
 
+    getByteCounts: () => ByteCounts
+
     reset: () => void
     destroy: () => void
 }
@@ -128,6 +136,25 @@ export function createResources(gl: GLRenderingContext, state: WebGLState, stats
             return wrap('vertexArray', createVertexArray(extensions, program, attributeBuffers, elementsBuffer));
         },
 
+        getByteCounts: () => {
+            let texture = 0;
+            sets.texture.forEach(r => {
+                texture += (r as Texture).getByteCount();
+            });
+
+            let attribute = 0;
+            sets.attribute.forEach(r => {
+                attribute += (r as AttributeBuffer).length * 4;
+            });
+
+            let elements = 0;
+            sets.elements.forEach(r => {
+                elements += (r as ElementsBuffer).length * 4;
+            });
+
+            return { texture, attribute, elements };
+        },
+
         reset: () => {
             sets.attribute.forEach(r => r.reset());
             sets.elements.forEach(r => r.reset());

+ 43 - 4
src/mol-gl/webgl/texture.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 Alexander Rose <alexander.rose@weirdbyte.de>
  */
@@ -19,14 +19,16 @@ const getNextTextureId = idFactory();
 export type TextureKindValue = {
     'image-uint8': TextureImage<Uint8Array>
     'image-float32': TextureImage<Float32Array>
+    'image-float16': TextureImage<Float32Array>
     'image-depth': TextureImage<Uint8Array> // TODO should be Uint32Array
     'volume-uint8': TextureVolume<Uint8Array>
     'volume-float32': TextureVolume<Float32Array>
+    'volume-float16': TextureVolume<Float32Array>
     'texture': Texture
 }
 export type TextureValueType = ValueOf<TextureKindValue>
 export type TextureKind = keyof TextureKindValue
-export type TextureType = 'ubyte' | 'ushort' | 'float'
+export type TextureType = 'ubyte' | 'ushort' | 'float' | 'fp16'
 export type TextureFormat = 'alpha' | 'rgb' | 'rgba' | 'depth'
 /** Numbers are shortcuts for color attachment */
 export type TextureAttachment = 'depth' | 'stencil' | 'color0' | 'color1' | 'color2' | 'color3' | 'color4' | 'color5' | 'color6' | 'color7' | 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7
@@ -36,12 +38,14 @@ export function getTarget(gl: GLRenderingContext, kind: TextureKind): number {
     switch (kind) {
         case 'image-uint8': return gl.TEXTURE_2D;
         case 'image-float32': return gl.TEXTURE_2D;
+        case 'image-float16': return gl.TEXTURE_2D;
         case 'image-depth': return gl.TEXTURE_2D;
     }
     if (isWebGL2(gl)) {
         switch (kind) {
             case 'volume-uint8': return gl.TEXTURE_3D;
             case 'volume-float32': return gl.TEXTURE_3D;
+            case 'volume-float16': return gl.TEXTURE_3D;
         }
     }
     throw new Error(`unknown texture kind '${kind}'`);
@@ -65,16 +69,19 @@ export function getInternalFormat(gl: GLRenderingContext, format: TextureFormat,
                 switch (type) {
                     case 'ubyte': return gl.ALPHA;
                     case 'float': return gl.R32F;
+                    case 'fp16': return gl.R16F;
                 }
             case 'rgb':
                 switch (type) {
                     case 'ubyte': return gl.RGB;
                     case 'float': return gl.RGB32F;
+                    case 'fp16': return gl.RGB16F;
                 }
             case 'rgba':
                 switch (type) {
                     case 'ubyte': return gl.RGBA;
                     case 'float': return gl.RGBA32F;
+                    case 'fp16': return gl.RGBA16F;
                 }
             case 'depth':
                 return gl.DEPTH_COMPONENT16;
@@ -83,11 +90,37 @@ export function getInternalFormat(gl: GLRenderingContext, format: TextureFormat,
     return getFormat(gl, format, type);
 }
 
-export function getType(gl: GLRenderingContext, type: TextureType): number {
+function getByteCount(format: TextureFormat, type: TextureType, width: number, height: number, depth: number): number {
+    const bpe = getFormatSize(format) * getTypeSize(type);
+    return bpe * width * height * (depth || 1);
+}
+
+function getFormatSize(format: TextureFormat) {
+    switch (format) {
+        case 'alpha': return 1;
+        case 'rgb': return 3;
+        case 'rgba': return 4;
+        case 'depth': return 4;
+    }
+}
+
+function getTypeSize(type: TextureType): number {
+    switch (type) {
+        case 'ubyte': return 1;
+        case 'ushort': return 2;
+        case 'float': return 4;
+        case 'fp16': return 2;
+    }
+}
+
+export function getType(gl: GLRenderingContext, extensions: WebGLExtensions, type: TextureType): number {
     switch (type) {
         case 'ubyte': return gl.UNSIGNED_BYTE;
         case 'ushort': return gl.UNSIGNED_SHORT;
         case 'float': return gl.FLOAT;
+        case 'fp16':
+            if (extensions.textureHalfFloat) return extensions.textureHalfFloat.HALF_FLOAT;
+            else throw new Error('extension "texture_half_float" unavailable');
     }
 }
 
@@ -141,6 +174,8 @@ export interface Texture {
     getHeight: () => number
     getDepth: () => number
 
+    getByteCount: () => number
+
     define: (width: number, height: number, depth?: number) => void
     load: (image: TextureImage<any> | TextureVolume<any> | HTMLImageElement) => void
     bind: (id: TextureId) => void
@@ -173,6 +208,7 @@ export function createTexture(gl: GLRenderingContext, extensions: WebGLExtension
     // check texture kind and type compatability
     if (
         (kind.endsWith('float32') && _type !== 'float') ||
+        (kind.endsWith('float16') && _type !== 'fp16') ||
         (kind.endsWith('uint8') && _type !== 'ubyte') ||
         (kind.endsWith('depth') && _type !== 'ushort')
     ) {
@@ -183,7 +219,7 @@ export function createTexture(gl: GLRenderingContext, extensions: WebGLExtension
     const filter = getFilter(gl, _filter);
     const format = getFormat(gl, _format, _type);
     const internalFormat = getInternalFormat(gl, _format, _type);
-    const type = getType(gl, _type);
+    const type = getType(gl, extensions, _type);
 
     function init() {
         gl.bindTexture(target, texture);
@@ -262,6 +298,8 @@ export function createTexture(gl: GLRenderingContext, extensions: WebGLExtension
         getHeight: () => height,
         getDepth: () => depth,
 
+        getByteCount: () => getByteCount(_format, _type, width, height, depth),
+
         define,
         load,
         bind: (id: TextureId) => {
@@ -349,6 +387,7 @@ export function createNullTexture(gl: GLRenderingContext, kind: TextureKind): Te
         getWidth: () => 0,
         getHeight: () => 0,
         getDepth: () => 0,
+        getByteCount: () => 0,
 
         define: () => {},
         load: () => {},

+ 1 - 1
src/mol-math/geometry/gaussian-density.ts

@@ -73,7 +73,7 @@ function _computeGaussianDensityTexture(type: '2d' | '3d', position: PositionDat
     if (!GaussianDensityTexture) throw 'GPU computation not supported on this platform';
     return Task.create('Gaussian Density', async ctx => {
         return type === '2d' ?
-            GaussianDensityTexture2d(webgl, position, box, radius, props, texture) :
+            GaussianDensityTexture2d(webgl, position, box, radius, false, props, texture) :
             GaussianDensityTexture3d(webgl, position, box, radius, props, texture);
     });
 }

+ 37 - 23
src/mol-math/geometry/gaussian-density/gpu.ts

@@ -1,5 +1,5 @@
 /**
- * Copyright (c) 2017-2020 mol* contributors, licensed under MIT, See LICENSE file for more info.
+ * Copyright (c) 2017-2021 mol* contributors, licensed under MIT, See LICENSE file for more info.
  *
  * @author Alexander Rose <alexander.rose@weirdbyte.de>
  * @author Michael Krone <michael.krone@uni-tuebingen.de>
@@ -40,7 +40,7 @@ export const GaussianDensitySchema = {
     uGridTexScale: UniformSpec('v2', true),
     uAlpha: UniformSpec('f', true),
     uResolution: UniformSpec('f', true),
-    uRadiusFactor: UniformSpec('f', true),
+    uRadiusFactorInv: UniformSpec('f', true),
     tMinDistanceTex: TextureSpec('texture', 'rgba', 'float', 'nearest'),
 
     dGridTexType: DefineSpec('string', ['2d', '3d']),
@@ -73,7 +73,7 @@ export function GaussianDensityGPU(position: PositionData, box: Box3D, radius: (
     // it's faster than texture3d
     // console.time('GaussianDensityTexture2d')
     const tmpTexture = getTexture('tmp', webgl, 'image-uint8', 'rgba', 'ubyte', 'linear');
-    const { scale, bbox, texture, gridDim, gridTexDim, radiusFactor } = calcGaussianDensityTexture2d(webgl, position, box, radius, props, tmpTexture);
+    const { scale, bbox, texture, gridDim, gridTexDim, radiusFactor } = calcGaussianDensityTexture2d(webgl, position, box, radius, false, props, tmpTexture);
     // webgl.waitForGpuCommandsCompleteSync()
     // console.timeEnd('GaussianDensityTexture2d')
     const { field, idField } = fieldFromTexture2d(webgl, texture, gridDim, gridTexDim);
@@ -84,11 +84,11 @@ export function GaussianDensityGPU(position: PositionData, box: Box3D, radius: (
 export function GaussianDensityTexture(webgl: WebGLContext, position: PositionData, box: Box3D, radius: (index: number) => number, props: GaussianDensityGPUProps, oldTexture?: Texture): GaussianDensityTextureData {
     return webgl.isWebGL2 ?
         GaussianDensityTexture3d(webgl, position, box, radius, props, oldTexture) :
-        GaussianDensityTexture2d(webgl, position, box, radius, props, oldTexture);
+        GaussianDensityTexture2d(webgl, position, box, radius, false, props, oldTexture);
 }
 
-export function GaussianDensityTexture2d(webgl: WebGLContext, position: PositionData, box: Box3D, radius: (index: number) => number, props: GaussianDensityGPUProps, oldTexture?: Texture): GaussianDensityTextureData {
-    return finalizeGaussianDensityTexture(calcGaussianDensityTexture2d(webgl, position, box, radius, props, oldTexture));
+export function GaussianDensityTexture2d(webgl: WebGLContext, position: PositionData, box: Box3D, radius: (index: number) => number, powerOfTwo: boolean, props: GaussianDensityGPUProps, oldTexture?: Texture): GaussianDensityTextureData {
+    return finalizeGaussianDensityTexture(calcGaussianDensityTexture2d(webgl, position, box, radius, powerOfTwo, props, oldTexture));
 }
 
 export function GaussianDensityTexture3d(webgl: WebGLContext, position: PositionData, box: Box3D, radius: (index: number) => number, props: GaussianDensityGPUProps, oldTexture?: Texture): GaussianDensityTextureData {
@@ -118,9 +118,9 @@ type _GaussianDensityTextureData = {
     radiusFactor: number
 }
 
-function calcGaussianDensityTexture2d(webgl: WebGLContext, position: PositionData, box: Box3D, radius: (index: number) => number, props: GaussianDensityGPUProps, texture?: Texture): _GaussianDensityTextureData {
+function calcGaussianDensityTexture2d(webgl: WebGLContext, position: PositionData, box: Box3D, radius: (index: number) => number, powerOfTwo: boolean, props: GaussianDensityGPUProps, texture?: Texture): _GaussianDensityTextureData {
     // console.log('2d');
-    const { gl, resources, state, extensions: { colorBufferFloat, textureFloat } } = webgl;
+    const { gl, resources, state, extensions: { colorBufferFloat, textureFloat, colorBufferHalfFloat, textureHalfFloat } } = webgl;
     const { smoothness, resolution } = props;
 
     const { drawCount, positions, radii, groups, scale, expandedBox, dim, maxRadius } = prepareGaussianDensityData(position, box, radius, props);
@@ -131,8 +131,11 @@ function calcGaussianDensityTexture2d(webgl: WebGLContext, position: PositionDat
     const gridTexScale = Vec2.create(texDimX / powerOfTwoSize, texDimY / powerOfTwoSize);
     const radiusFactor = maxRadius * 2;
 
+    const width = powerOfTwo ? powerOfTwoSize : texDimX;
+    const height = powerOfTwo ? powerOfTwoSize : texDimY;
+
     const minDistTex = getTexture('min-dist-2d', webgl, 'image-uint8', 'rgba', 'ubyte', 'nearest');
-    minDistTex.define(powerOfTwoSize, powerOfTwoSize);
+    minDistTex.define(width, height);
 
     const renderable = getGaussianDensityRenderable(webgl, drawCount, positions, radii, groups, minDistTex, expandedBox, dim, gridTexDim, gridTexScale, smoothness, resolution, radiusFactor);
 
@@ -144,17 +147,24 @@ function calcGaussianDensityTexture2d(webgl: WebGLContext, position: PositionDat
     framebuffer.bind();
     setRenderingDefaults(webgl);
 
-    if (!texture) texture = colorBufferFloat && textureFloat
-        ? resources.texture('image-float32', 'rgba', 'float', 'linear')
-        : resources.texture('image-uint8', 'rgba', 'ubyte', 'linear');
-    texture.define(powerOfTwoSize, powerOfTwoSize);
+    if (!texture) texture = colorBufferHalfFloat && textureHalfFloat
+        ? resources.texture('image-float16', 'rgba', 'fp16', 'linear')
+        : colorBufferFloat && textureFloat
+            ? resources.texture('image-float32', 'rgba', 'float', 'linear')
+            : resources.texture('image-uint8', 'rgba', 'ubyte', 'linear');
+    texture.define(width, height);
 
     // console.log(renderable)
 
     function render(fbTex: Texture, clear: boolean) {
         state.currentRenderItemId = -1;
         fbTex.attachFramebuffer(framebuffer, 0);
-        if (clear) gl.clear(gl.COLOR_BUFFER_BIT);
+        if (clear) {
+            gl.viewport(0, 0, width, height);
+            gl.scissor(0, 0, width, height);
+            gl.clear(gl.COLOR_BUFFER_BIT);
+        }
+        ValueCell.update(uCurrentY, 0);
         let currCol = 0;
         let currY = 0;
         let currX = 0;
@@ -165,10 +175,11 @@ function calcGaussianDensityTexture2d(webgl: WebGLContext, position: PositionDat
                 currX = 0;
                 ValueCell.update(uCurrentY, currY);
             }
-            // console.log({ i, currX, currY })
+            // console.log({ i, currX, currY });
             ValueCell.update(uCurrentX, currX);
             ValueCell.update(uCurrentSlice, i);
             gl.viewport(currX, currY, dx, dy);
+            gl.scissor(currX, currY, dx, dy);
             renderable.render();
             ++currCol;
             currX += dx;
@@ -185,14 +196,14 @@ function calcGaussianDensityTexture2d(webgl: WebGLContext, position: PositionDat
     setupGroupIdRendering(webgl, renderable);
     render(texture, false);
 
-    // printTexture(webgl, texture, 1);
+    // printTexture(webgl, minDistTex, 0.75);
 
     return { texture, scale, bbox: expandedBox, gridDim: dim, gridTexDim, gridTexScale, radiusFactor };
 }
 
 function calcGaussianDensityTexture3d(webgl: WebGLContext, position: PositionData, box: Box3D, radius: (index: number) => number, props: GaussianDensityGPUProps, texture?: Texture): _GaussianDensityTextureData {
     // console.log('3d');
-    const { gl, resources, state, extensions: { colorBufferFloat, textureFloat } } = webgl;
+    const { gl, resources, state, extensions: { colorBufferFloat, textureFloat, colorBufferHalfFloat, textureHalfFloat } } = webgl;
     const { smoothness, resolution } = props;
 
     const { drawCount, positions, radii, groups, scale, expandedBox, dim, maxRadius } = prepareGaussianDensityData(position, box, radius, props);
@@ -214,10 +225,13 @@ function calcGaussianDensityTexture3d(webgl: WebGLContext, position: PositionDat
     framebuffer.bind();
     setRenderingDefaults(webgl);
     gl.viewport(0, 0, dx, dy);
+    gl.scissor(0, 0, dx, dy);
 
-    if (!texture) texture = colorBufferFloat && textureFloat
-        ? resources.texture('volume-float32', 'rgba', 'float', 'linear')
-        : resources.texture('volume-uint8', 'rgba', 'ubyte', 'linear');
+    if (!texture) texture = colorBufferHalfFloat && textureHalfFloat
+        ? resources.texture('volume-float16', 'rgba', 'fp16', 'linear')
+        : colorBufferFloat && textureFloat
+            ? resources.texture('volume-float32', 'rgba', 'float', 'linear')
+            : resources.texture('volume-uint8', 'rgba', 'ubyte', 'linear');
     texture.define(dx, dy, dz);
 
     function render(fbTex: Texture, clear: boolean) {
@@ -304,7 +318,7 @@ function getGaussianDensityRenderable(webgl: WebGLContext, drawCount: number, po
         ValueCell.update(v.uGridTexScale, gridTexScale);
         ValueCell.updateIfChanged(v.uAlpha, smoothness);
         ValueCell.updateIfChanged(v.uResolution, resolution);
-        ValueCell.updateIfChanged(v.uRadiusFactor, radiusFactor);
+        ValueCell.updateIfChanged(v.uRadiusFactorInv, 1 / radiusFactor);
         ValueCell.update(v.tMinDistanceTex, minDistanceTexture);
 
         ValueCell.updateIfChanged(v.dGridTexType, minDistanceTexture.getDepth() > 0 ? '3d' : '2d');
@@ -338,7 +352,7 @@ function createGaussianDensityRenderable(webgl: WebGLContext, drawCount: number,
         uGridTexScale: ValueCell.create(gridTexScale),
         uAlpha: ValueCell.create(smoothness),
         uResolution: ValueCell.create(resolution),
-        uRadiusFactor: ValueCell.create(radiusFactor),
+        uRadiusFactorInv: ValueCell.create(1 / radiusFactor),
         tMinDistanceTex: ValueCell.create(minDistanceTexture),
 
         dGridTexType: ValueCell.create(minDistanceTexture.getDepth() > 0 ? '3d' : '2d'),
@@ -356,7 +370,7 @@ function setRenderingDefaults(ctx: WebGLContext) {
     state.disable(gl.CULL_FACE);
     state.enable(gl.BLEND);
     state.disable(gl.DEPTH_TEST);
-    state.disable(gl.SCISSOR_TEST);
+    state.enable(gl.SCISSOR_TEST);
     state.depthMask(false);
     state.clearColor(0, 0, 0, 0);
 }

+ 3 - 3
src/mol-model-formats/structure/basic/schema.ts

@@ -24,9 +24,9 @@ export type ChemComp = Table<mmCIF_chemComp_schema>
 export type ChemCompIdentifier = Table<mmCIF_Schema['pdbx_chem_comp_identifier']>
 export type AtomSite = Table<mmCIF_Schema['atom_site']>
 export type IhmSphereObjSite = Table<mmCIF_Schema['ihm_sphere_obj_site']>
-export type IhmGaussianObjSite =Table<mmCIF_Schema['ihm_gaussian_obj_site']>
-export type UnobsOrZeroOccResidues =Table<mmCIF_Schema['pdbx_unobs_or_zero_occ_residues']>
-export type Molecule =Table<mmCIF_Schema['pdbx_molecule']>
+export type IhmGaussianObjSite = Table<mmCIF_Schema['ihm_gaussian_obj_site']>
+export type UnobsOrZeroOccResidues = Table<mmCIF_Schema['pdbx_unobs_or_zero_occ_residues']>
+export type Molecule = Table<mmCIF_Schema['pdbx_molecule']>
 
 export const BasicSchema = {
     entry: mmCIF_Schema.entry,

+ 89 - 0
src/mol-model-formats/structure/pdb/conect.ts

@@ -0,0 +1,89 @@
+/**
+ * Copyright (c) 2021 mol* contributors, licensed under MIT, See LICENSE file for more info.
+ *
+ * @author Alexander Rose <alexander.rose@weirdbyte.de>
+ */
+
+import { CifCategory, CifField } from '../../../mol-io/reader/cif';
+import { mmCIF_Schema } from '../../../mol-io/reader/cif/schema/mmcif';
+import { Tokens } from '../../../mol-io/reader/common/text/tokenizer';
+
+export function parseConect(lines: Tokens, lineStart: number, lineEnd: number, sites: { [K in keyof mmCIF_Schema['atom_site']]?: CifField }): CifCategory {
+    const idMap: { [k: string]: number } = {};
+    for (let i = 0, il = sites.id!.rowCount; i < il; ++i) {
+        idMap[sites.id!.str(i)] = i;
+    }
+
+    const getLine = (n: number) => lines.data.substring(lines.indices[2 * n], lines.indices[2 * n + 1]);
+
+    const id: string[] = [];
+    const conn_type_id: string[] = [];
+
+    const ptnr1_label_asym_id: string[] = [];
+    const ptnr1_label_seq_id: number[] = [];
+    const ptnr1_auth_seq_id: number[] = [];
+    const ptnr1_label_atom_id: string[] = [];
+
+    const ptnr2_label_asym_id: string[] = [];
+    const ptnr2_label_seq_id: number[] = [];
+    const ptnr2_auth_seq_id: number[] = [];
+    const ptnr2_label_atom_id: string[] = [];
+
+    const pos = [11, 16, 21, 26];
+
+    let k = 1;
+
+    for (let i = lineStart; i < lineEnd; i++) {
+        const line = getLine(i);
+        const idxA = idMap[parseInt(line.substr(6, 5))];
+
+        const bondIndex: {[k: number]: number} = {};
+
+        if (idxA === undefined) continue;
+
+        for (let j = 0; j < 4; ++j) {
+            const idB = parseInt(line.substr(pos[j], 5));
+            if (Number.isNaN(idB)) continue;
+
+            const idxB = idMap[idB];
+            if (idxB === undefined) continue;
+            if (idxA > idxB) continue;
+
+            // TODO: interpret records where a 'idxB' atom is given multiple times
+            // as double/triple bonds, e.g. CONECT 1529 1528 1528 is a double bond
+            if (bondIndex[idxB] !== undefined) continue;
+
+            id.push(`covale${k}`);
+            conn_type_id.push('covale');
+
+            ptnr1_label_asym_id.push(sites.label_asym_id!.str(idxA));
+            ptnr1_auth_seq_id.push(sites.auth_seq_id!.int(idxA));
+            ptnr1_label_seq_id.push(sites.label_seq_id!.int(idxA));
+            ptnr1_label_atom_id.push(sites.label_atom_id!.str(idxA));
+
+            ptnr2_label_asym_id.push(sites.label_asym_id!.str(idxB));
+            ptnr2_auth_seq_id.push(sites.auth_seq_id!.int(idxB));
+            ptnr2_label_seq_id.push(sites.label_seq_id!.int(idxB));
+            ptnr2_label_atom_id.push(sites.label_atom_id!.str(idxB));
+
+            k += 1;
+        }
+    }
+
+    const struct_conn: Partial<CifCategory.Fields<mmCIF_Schema['struct_conn']>> = {
+        id: CifField.ofStrings(id),
+        conn_type_id: CifField.ofStrings(conn_type_id),
+
+        ptnr1_label_asym_id: CifField.ofStrings(ptnr1_label_asym_id),
+        ptnr1_auth_seq_id: CifField.ofNumbers(ptnr1_auth_seq_id),
+        ptnr1_label_seq_id: CifField.ofNumbers(ptnr1_label_seq_id),
+        ptnr1_label_atom_id: CifField.ofStrings(ptnr1_label_atom_id),
+
+        ptnr2_label_asym_id: CifField.ofStrings(ptnr2_label_asym_id),
+        ptnr2_label_seq_id: CifField.ofNumbers(ptnr2_label_seq_id),
+        ptnr2_auth_seq_id: CifField.ofNumbers(ptnr2_auth_seq_id),
+        ptnr2_label_atom_id: CifField.ofStrings(ptnr2_label_atom_id),
+    };
+
+    return CifCategory.ofFields('struct_conn', struct_conn);
+}

+ 23 - 3
src/mol-model-formats/structure/pdb/to-cif.ts

@@ -1,5 +1,5 @@
 /**
- * Copyright (c) 2019-2020 mol* contributors, licensed under MIT, See LICENSE file for more info.
+ * Copyright (c) 2019-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>
@@ -18,6 +18,8 @@ import { Column } from '../../../mol-data/db';
 import { getMoleculeType } from '../../../mol-model/structure/model/types';
 import { getAtomSiteTemplate, addAtom, getAtomSite } from './atom-site';
 import { addAnisotropic, getAnisotropicTemplate, getAnisotropic } from './anisotropic';
+import { parseConect } from './conect';
+import { isDebugMode } from '../../../mol-util/debug';
 
 export async function pdbToMmCif(pdb: PdbFile): Promise<CifFrame> {
     const { lines } = pdb;
@@ -48,6 +50,7 @@ export async function pdbToMmCif(pdb: PdbFile): Promise<CifFrame> {
     const heteroNames: [string, string][] = [];
 
     let modelNum = 0, modelStr = '';
+    let conectRange: [number, number] | undefined = undefined;
 
     for (let i = 0, _i = lines.count; i < _i; i++) {
         let s = indices[2 * i], e = indices[2 * i + 1];
@@ -63,8 +66,21 @@ export async function pdbToMmCif(pdb: PdbFile): Promise<CifFrame> {
             case 'C':
                 if (substringStartsWith(data, s, e, 'CRYST1')) {
                     helperCategories.push(...parseCryst1(pdb.id || '?', data.substring(s, e)));
-                } else if (substringStartsWith(data, s, e, 'CONNECT')) {
-                    // TODO: CONNECT records => struct_conn
+                } else if (substringStartsWith(data, s, e, 'CONECT')) {
+                    let j = i + 1;
+                    while (true) {
+                        s = indices[2 * j]; e = indices[2 * j + 1];
+                        if (!substringStartsWith(data, s, e, 'CONECT')) break;
+                        j++;
+                    }
+                    if (conectRange) {
+                        if (isDebugMode) {
+                            console.log('only single CONECT block allowed, ignoring others');
+                        }
+                    } else {
+                        conectRange = [i, j];
+                    }
+                    i = j - 1;
                 } else if (substringStartsWith(data, s, e, 'COMPND')) {
                     let j = i + 1;
                     while (true) {
@@ -165,6 +181,10 @@ export async function pdbToMmCif(pdb: PdbFile): Promise<CifFrame> {
     const atom_site = getAtomSite(atomSite);
     if (!isPdbqt) delete atom_site.partial_charge;
 
+    if (conectRange) {
+        helperCategories.push(parseConect(lines, conectRange[0], conectRange[1], atom_site));
+    }
+
     const categories = {
         entity: CifCategory.ofTable('entity', entityBuilder.getEntityTable()),
         chem_comp: CifCategory.ofTable('chem_comp', componentBuilder.getChemCompTable()),

+ 10 - 1
src/mol-model-formats/structure/property/bonds/struct_conn.ts

@@ -1,5 +1,5 @@
 /**
- * Copyright (c) 2017-2020 Mol* contributors, licensed under MIT, See LICENSE file for more info.
+ * Copyright (c) 2017-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>
@@ -52,6 +52,15 @@ export namespace StructConn {
 
     export const Provider = FormatPropertyProvider.create<StructConn>(Descriptor);
 
+    /**
+     * Heuristic to test if StructConn likely provides all atomic bonds by
+     * checking if the fraction of bonds and atoms is high (> 0.95).
+     */
+    export function isExhaustive(model: Model): boolean {
+        const structConn = StructConn.Provider.get(model);
+        return !!structConn && (structConn.data.id.rowCount / model.atomicConformation.atomId.rowCount) > 0.95;
+    }
+
     function hasAtom({ units }: Structure, element: ElementIndex) {
         for (let i = 0, _i = units.length; i < _i; i++) {
             if (SortedArray.indexOf(units[i].elements, element) >= 0) return true;

+ 3 - 2
src/mol-model/structure/model/properties/common.ts

@@ -1,5 +1,5 @@
 /**
- * Copyright (c) 2018-2019 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 Alexander Rose <alexander.rose@weirdbyte.de>
  * @author David Sehnal <david.sehnal@gmail.com>
@@ -14,7 +14,8 @@ export type EntitySubtype = (
     mmCIF_Schema['entity_poly']['type']['T'] |
     mmCIF_Schema['pdbx_entity_branch']['type']['T'] |
     'ion' |
-    'lipid'
+    'lipid' |
+    'peptide-like'
 )
 export const EntitySubtype = Column.Schema.Aliased<EntitySubtype>(Column.Schema.Str(''));
 

+ 4 - 2
src/mol-model/structure/model/types.ts

@@ -1,5 +1,5 @@
 /**
- * Copyright (c) 2017-2020 mol* contributors, licensed under MIT, See LICENSE file for more info.
+ * Copyright (c) 2017-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>
@@ -192,7 +192,7 @@ export const ProteinTerminusComponentTypeNames = new Set([
     'L-PEPTIDE NH3 AMINO TERMINUS', 'L-PEPTIDE COOH CARBOXY TERMINUS'
 ]);
 
-/** Chemical component type names for pepdite-like protein */
+/** Chemical component type names for peptide-like protein */
 export const OtherProteinComponentTypeNames = new Set([
     'PEPTIDE LINKING', 'PEPTIDE-LIKE',
 ]);
@@ -416,6 +416,8 @@ export function getEntitySubtype(compId: string, compType: string): EntitySubtyp
         return 'ion';
     } else if (LipidComponentTypeNames.has(compType) || LipidNames.has(compId)) {
         return 'lipid';
+    } else if (OtherProteinComponentTypeNames.has(compType)) {
+        return 'peptide-like';
     } else {
         return 'other';
     }

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

@@ -97,7 +97,7 @@ class Structure {
 
     /** Count of all bonds (intra- and inter-unit) in the structure */
     get bondCount() {
-        if (!this._props.bondCount) {
+        if (this._props.bondCount === -1) {
             this._props.bondCount = this.interUnitBonds.edgeCount + Bond.getIntraUnitBondCount(this);
         }
         return this._props.bondCount;

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

@@ -162,7 +162,7 @@ namespace Bond {
         let count = 0;
         for (let i = 0, il = structure.units.length; i < il; ++i) {
             const u = structure.units[i];
-            if (Unit.isAtomic(u)) count += u.bonds.edgeCount / 2; // only count one direction
+            if (Unit.isAtomic(u)) count += u.bonds.edgeCount;
         }
         return count;
     }

+ 3 - 0
src/mol-model/structure/structure/unit/bonds/inter-compute.ts

@@ -50,6 +50,8 @@ function findPairBonds(unitA: Unit.Atomic, unitB: Unit.Atomic, props: BondComput
     const structConn = unitA.model === unitB.model && StructConn.Provider.get(unitA.model);
     const indexPairs = unitA.model === unitB.model && IndexPairBonds.Provider.get(unitA.model);
 
+    const structConnExhaustive = unitA.model === unitB.model && StructConn.isExhaustive(unitA.model);
+
     // the lookup queries need to happen in the "unitB space".
     // that means _imageA = inverseOperB(operA(aI))
     const imageTransform = Mat4.mul(_imageTransform, unitB.conformation.operator.inverse, unitA.conformation.operator.matrix);
@@ -103,6 +105,7 @@ function findPairBonds(unitA: Unit.Atomic, unitB: Unit.Atomic, props: BondComput
             // all are given and thus we don't need to compute any other
             if (added) continue;
         }
+        if (structConnExhaustive) continue;
 
         const occA = occupancyA.value(aI);
 

+ 3 - 0
src/mol-model/structure/structure/unit/bonds/intra-compute.ts

@@ -95,6 +95,8 @@ function findBonds(unit: Unit.Atomic, props: BondComputationProps): IntraUnitBon
     const structConn = StructConn.Provider.get(unit.model);
     const component = ComponentBond.Provider.get(unit.model);
 
+    const structConnExhaustive = StructConn.isExhaustive(unit.model);
+
     const atomA: StructureElement.UnitIndex[] = [];
     const atomB: StructureElement.UnitIndex[] = [];
     const flags: number[] = [];
@@ -135,6 +137,7 @@ function findBonds(unit: Unit.Atomic, props: BondComputationProps): IntraUnitBon
                 structConnAdded.add(_bI);
             }
         }
+        if (structConnExhaustive) continue;
 
         const raI = residueIndex[aI];
         const seqIdA = label_seq_id.value(raI);

+ 10 - 4
src/mol-plugin-state/builder/structure/representation-preset.ts

@@ -1,5 +1,5 @@
 /**
- * Copyright (c) 2019-2020 mol* contributors, licensed under MIT, See LICENSE file for more info.
+ * Copyright (c) 2019-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>
@@ -20,6 +20,8 @@ import { StructureFocusRepresentation } from '../../../mol-plugin/behavior/dynam
 import { createStructureColorThemeParams } from '../../helpers/structure-representation-params';
 import { ChainIdColorThemeProvider } from '../../../mol-theme/color/chain-id';
 import { OperatorNameColorThemeProvider } from '../../../mol-theme/color/operator-name';
+import { IndexPairBonds } from '../../../mol-model-formats/structure/property/bonds/index-pair';
+import { StructConn } from '../../../mol-model-formats/structure/property/bonds/struct_conn';
 
 export interface StructureRepresentationPresetProvider<P = any, S extends _Result = _Result> extends PresetProvider<PluginStateObject.Molecule.Structure, P, S> { }
 export function StructureRepresentationPresetProvider<P, S extends _Result>(repr: StructureRepresentationPresetProvider<P, S>) { return repr; }
@@ -332,8 +334,12 @@ const atomicDetail = StructureRepresentationPresetProvider({
             structure.elementCount > 1000 &&
             structure.atomicResidueCount / structure.elementCount < 3;
 
-        const atomicType = lowResidueElementRatio ? 'spacefill' :
-            highElementCount ? 'line' : 'ball-and-stick';
+        const m = structure.models[0];
+        const bondsGiven = !!IndexPairBonds.Provider.get(m) || StructConn.isExhaustive(m);
+
+        const atomicType = lowResidueElementRatio && !bondsGiven
+            ? 'spacefill' : highElementCount
+                ? 'line' : 'ball-and-stick';
         const showCarbohydrateSymbol = params.showCarbohydrateSymbol && !highElementCount && !lowResidueElementRatio;
 
         if (showCarbohydrateSymbol) {
@@ -343,7 +349,7 @@ const atomicDetail = StructureRepresentationPresetProvider({
         }
 
         const { update, builder, typeParams, color, ballAndStickColor } = reprBuilder(plugin, params, structure);
-        const colorParams = lowResidueElementRatio
+        const colorParams = lowResidueElementRatio && !bondsGiven
             ? { carbonColor: { name: 'element-symbol', params: {} } }
             : ballAndStickColor;
 

+ 2 - 2
src/mol-plugin-state/helpers/structure-selection-query.ts

@@ -92,7 +92,7 @@ const polymer = StructureSelectionQuery('Polymer', MS.struct.modifier.union([
         'entity-test': MS.core.logic.and([
             MS.core.rel.eq([MS.ammp('entityType'), 'polymer']),
             MS.core.str.match([
-                MS.re('(polypeptide|cyclic-pseudo-peptide|nucleotide|peptide nucleic acid)', 'i'),
+                MS.re('(polypeptide|cyclic-pseudo-peptide|peptide-like|nucleotide|peptide nucleic acid)', 'i'),
                 MS.ammp('entitySubtype')
             ])
         ])
@@ -122,7 +122,7 @@ const trace = StructureSelectionQuery('Trace', MS.struct.modifier.union([
 const _proteinEntityTest = MS.core.logic.and([
     MS.core.rel.eq([MS.ammp('entityType'), 'polymer']),
     MS.core.str.match([
-        MS.re('(polypeptide|cyclic-pseudo-peptide)', 'i'),
+        MS.re('(polypeptide|cyclic-pseudo-peptide|peptide-like)', 'i'),
         MS.ammp('entitySubtype')
     ])
 ]);

+ 107 - 37
src/mol-plugin-ui/sequence.tsx

@@ -22,6 +22,7 @@ import { ElementSequenceWrapper } from './sequence/element';
 import { elementLabel } from '../mol-theme/label';
 import { Icon, HelpOutlineSvg } from './controls/icons';
 import { StructureSelectionManager } from '../mol-plugin-state/manager/structure/selection';
+import { arrayEqual } from '../mol-util/array';
 
 const MaxDisplaySequenceLength = 5000;
 
@@ -34,14 +35,14 @@ function opKey(l: StructureElement.Location) {
 }
 
 function splitModelEntityId(modelEntityId: string) {
-    const [ modelIdx, entityId ] = modelEntityId.split('|');
-    return [ parseInt(modelIdx), entityId ];
+    const [modelIdx, entityId] = modelEntityId.split('|');
+    return [parseInt(modelIdx), entityId];
 }
 
-function getSequenceWrapper(state: SequenceViewState, structureSelection: StructureSelectionManager): SequenceWrapper.Any | string {
+function getSequenceWrapper(state: { structure: Structure, modelEntityId: string, chainGroupId: number, operatorKey: string }, structureSelection: StructureSelectionManager): SequenceWrapper.Any | string {
     const { structure, modelEntityId, chainGroupId, operatorKey } = state;
     const l = StructureElement.Location.create(structure);
-    const [ modelIdx, entityId ] = splitModelEntityId(modelEntityId);
+    const [modelIdx, entityId] = splitModelEntityId(modelEntityId);
 
     const units: Unit[] = [];
 
@@ -93,7 +94,7 @@ function getSequenceWrapper(state: SequenceViewState, structureSelection: Struct
     }
 }
 
-function getModelEntityOptions(structure: Structure) {
+function getModelEntityOptions(structure: Structure, polymersOnly = false) {
     const options: [string, string][] = [];
     const l = StructureElement.Location.create(structure);
     const seen = new Set<string>();
@@ -104,17 +105,18 @@ function getModelEntityOptions(structure: Structure) {
         const modelIdx = structure.getModelIndex(unit.model);
         const key = `${modelIdx}|${id}`;
         if (seen.has(key)) continue;
+        if (polymersOnly && SP.entity.type(l) !== 'polymer') continue;
 
         let description = SP.entity.pdbx_description(l).join(', ');
         if (structure.models.length) {
             if (structure.representativeModel) { // indicates model trajectory
                 description += ` (Model ${structure.models[modelIdx].modelNum})`;
-            } else  if (description.startsWith('Polymer ')) { // indicates generic entity name
+            } else if (description.startsWith('Polymer ')) { // indicates generic entity name
                 description += ` (${structure.models[modelIdx].entry})`;
             }
         }
         const label = `${id}: ${description}`;
-        options.push([ key, label ]);
+        options.push([key, label]);
         seen.add(key);
     }
 
@@ -126,7 +128,7 @@ function getChainOptions(structure: Structure, modelEntityId: string) {
     const options: [number, string][] = [];
     const l = StructureElement.Location.create(structure);
     const seen = new Set<number>();
-    const [ modelIdx, entityId ] = splitModelEntityId(modelEntityId);
+    const [modelIdx, entityId] = splitModelEntityId(modelEntityId);
 
     for (const unit of structure.units) {
         StructureElement.Location.set(l, structure, unit, unit.elements[0]);
@@ -140,7 +142,7 @@ function getChainOptions(structure: Structure, modelEntityId: string) {
         // - more than one chain in a unit
         let label = elementLabel(l, { granularity: 'chain', hidePrefix: true, htmlStyling: false });
 
-        options.push([ id, label ]);
+        options.push([id, label]);
         seen.add(id);
     }
 
@@ -152,7 +154,7 @@ function getOperatorOptions(structure: Structure, modelEntityId: string, chainGr
     const options: [string, string][] = [];
     const l = StructureElement.Location.create(structure);
     const seen = new Set<string>();
-    const [ modelIdx, entityId ] = splitModelEntityId(modelEntityId);
+    const [modelIdx, entityId] = splitModelEntityId(modelEntityId);
 
     for (const unit of structure.units) {
         StructureElement.Location.set(l, structure, unit, unit.elements[0]);
@@ -164,7 +166,7 @@ function getOperatorOptions(structure: Structure, modelEntityId: string, chainGr
         if (seen.has(id)) continue;
 
         const label = unit.conformation.operator.name;
-        options.push([ id, label ]);
+        options.push([id, label]);
         seen.add(id);
     }
 
@@ -174,49 +176,64 @@ function getOperatorOptions(structure: Structure, modelEntityId: string, chainGr
 
 function getStructureOptions(state: State) {
     const options: [string, string][] = [];
+    const all: Structure[] = [];
 
     const structures = state.select(StateSelection.Generators.rootsOfType(PSO.Molecule.Structure));
     for (const s of structures) {
+        if (!s.obj?.data) continue;
+
+        all.push(s.obj.data);
         options.push([s.transform.ref, s.obj!.data.label]);
     }
 
     if (options.length === 0) options.push(['', 'No structure']);
-    return options;
+    return { options, all };
 }
 
+export type SequenceViewMode = 'single' | 'polymers' | 'all'
+const SequenceViewModeParam = PD.Select<SequenceViewMode>('single', [['single', 'Chain'], ['polymers', 'Polymers'], ['all', 'Everything']]);
+
 type SequenceViewState = {
+    structureOptions: { options: [string, string][], all: Structure[] },
     structure: Structure,
     structureRef: string,
     modelEntityId: string,
     chainGroupId: number,
-    operatorKey: string
+    operatorKey: string,
+    mode: SequenceViewMode
 }
 
-export class SequenceView extends PluginUIComponent<{ }, SequenceViewState> {
-    state = { structure: Structure.Empty, structureRef: '', modelEntityId: '', chainGroupId: -1, operatorKey: '' }
+export class SequenceView extends PluginUIComponent<{ defaultMode?: SequenceViewMode }, SequenceViewState> {
+    state: SequenceViewState = { structureOptions: { options: [], all: [] }, structure: Structure.Empty, structureRef: '', modelEntityId: '', chainGroupId: -1, operatorKey: '', mode: 'single' }
 
     componentDidMount() {
         if (this.plugin.state.data.select(StateSelection.Generators.rootsOfType(PSO.Molecule.Structure)).length > 0) this.setState(this.getInitialState());
 
         this.subscribe(this.plugin.state.events.object.updated, ({ ref, obj }) => {
             if (ref === this.state.structureRef && obj && obj.type === PSO.Molecule.Structure.type && obj.data !== this.state.structure) {
-                this.setState(this.getInitialState());
+                this.sync();
             }
         });
 
         this.subscribe(this.plugin.state.events.object.created, ({ obj }) => {
             if (obj && obj.type === PSO.Molecule.Structure.type) {
-                this.setState(this.getInitialState());
+                this.sync();
             }
         });
 
         this.subscribe(this.plugin.state.events.object.removed, ({ obj }) => {
             if (obj && obj.type === PSO.Molecule.Structure.type && obj.data === this.state.structure) {
-                this.setState(this.getInitialState());
+                this.sync();
             }
         });
     }
 
+    private sync() {
+        const structureOptions = getStructureOptions(this.plugin.state.data);
+        if (arrayEqual(structureOptions.all, this.state.structureOptions.all)) return;
+        this.setState(this.getInitialState());
+    }
+
     private getStructure(ref: string) {
         const state = this.plugin.state.data;
         const cell = state.select(ref)[0];
@@ -224,12 +241,40 @@ export class SequenceView extends PluginUIComponent<{ }, SequenceViewState> {
         return (cell.obj as PSO.Molecule.Structure).data;
     }
 
-    private getSequenceWrapper() {
-        return getSequenceWrapper(this.state, this.plugin.managers.structure.selection);
+    private getSequenceWrapper(params: SequenceView['params']) {
+        return {
+            wrapper: getSequenceWrapper(this.state, this.plugin.managers.structure.selection),
+            label: `${PD.optionLabel(params.chain, this.state.chainGroupId)} | ${PD.optionLabel(params.entity, this.state.modelEntityId)}`
+        };
+    }
+
+    private getSequenceWrappers(params: SequenceView['params']) {
+        if (this.state.mode === 'single') return [this.getSequenceWrapper(params)];
+
+        const structure = this.getStructure(this.state.structureRef);
+        const wrappers: { wrapper: (string | SequenceWrapper.Any), label: string }[] = [];
+
+        for (const [modelEntityId, eLabel] of getModelEntityOptions(structure, this.state.mode === 'polymers')) {
+            for (const [chainGroupId, cLabel] of getChainOptions(structure, modelEntityId)) {
+                for (const [operatorKey] of getOperatorOptions(structure, modelEntityId, chainGroupId)) {
+                    wrappers.push({
+                        wrapper: getSequenceWrapper({
+                            structure,
+                            modelEntityId,
+                            chainGroupId,
+                            operatorKey
+                        }, this.plugin.managers.structure.selection),
+                        label: `${cLabel} | ${eLabel}`
+                    });
+                }
+            }
+        }
+        return wrappers;
     }
 
     private getInitialState(): SequenceViewState {
-        const structureRef = getStructureOptions(this.plugin.state.data)[0][0];
+        const structureOptions = getStructureOptions(this.plugin.state.data);
+        const structureRef = structureOptions.options[0][0];
         const structure = this.getStructure(structureRef);
         let modelEntityId = getModelEntityOptions(structure)[0][0];
         let chainGroupId = getChainOptions(structure, modelEntityId)[0][0];
@@ -239,20 +284,20 @@ export class SequenceView extends PluginUIComponent<{ }, SequenceViewState> {
             chainGroupId = this.state.chainGroupId;
             operatorKey = this.state.operatorKey;
         }
-        return { structure, structureRef, modelEntityId, chainGroupId, operatorKey };
+        return { structureOptions, structure, structureRef, modelEntityId, chainGroupId, operatorKey, mode: this.props.defaultMode ?? 'single' };
     }
 
     private get params() {
-        const { structure, modelEntityId, chainGroupId } = this.state;
-        const structureOptions = getStructureOptions(this.plugin.state.data);
+        const { structureOptions, structure, modelEntityId, chainGroupId } = this.state;
         const entityOptions = getModelEntityOptions(structure);
         const chainOptions = getChainOptions(structure, modelEntityId);
         const operatorOptions = getOperatorOptions(structure, modelEntityId, chainGroupId);
         return {
-            structure: PD.Select(structureOptions[0][0], structureOptions, { shortLabel: true }),
+            structure: PD.Select(structureOptions.options[0][0], structureOptions.options, { shortLabel: true }),
             entity: PD.Select(entityOptions[0][0], entityOptions, { shortLabel: true }),
             chain: PD.Select(chainOptions[0][0], chainOptions, { shortLabel: true, twoColumns: true, label: 'Chain' }),
-            operator: PD.Select(operatorOptions[0][0], operatorOptions, { shortLabel: true, twoColumns: true })
+            operator: PD.Select(operatorOptions[0][0], operatorOptions, { shortLabel: true, twoColumns: true }),
+            mode: SequenceViewModeParam
         };
     }
 
@@ -261,16 +306,24 @@ export class SequenceView extends PluginUIComponent<{ }, SequenceViewState> {
             structure: this.state.structureRef,
             entity: this.state.modelEntityId,
             chain: this.state.chainGroupId,
-            operator: this.state.operatorKey
+            operator: this.state.operatorKey,
+            mode: this.state.mode
         };
     }
 
     private setParamProps = (p: { param: PD.Base<any>, name: string, value: any }) => {
         const state = { ...this.state };
         switch (p.name) {
+            case 'mode':
+                state.mode = p.value;
+                if (this.state.mode === state.mode) return;
+
+                if (state.mode === 'all' || state.mode === 'polymers') {
+                    break;
+                }
             case 'structure':
-                state.structureRef = p.value;
-                state.structure = this.getStructure(p.value);
+                if (p.name === 'structure') state.structureRef = p.value;
+                state.structure = this.getStructure(state.structureRef);
                 state.modelEntityId = getModelEntityOptions(state.structure)[0][0];
                 state.chainGroupId = getChainOptions(state.structure, state.modelEntityId)[0][0];
                 state.operatorKey = getOperatorOptions(state.structure, state.modelEntityId, state.chainGroupId)[0][0];
@@ -296,17 +349,16 @@ export class SequenceView extends PluginUIComponent<{ }, SequenceViewState> {
             return <div className='msp-sequence'>
                 <div className='msp-sequence-select'>
                     <Icon svg={HelpOutlineSvg} style={{ cursor: 'help', position: 'absolute', right: 0, top: 0 }}
-                        title='This shows a single sequence. Use the controls to show a different sequence.'/>
+                        title='Shows a sequence of one or more chains. Use the controls to alter selection.' />
 
                     <span>Sequence</span><span style={{ fontWeight: 'normal' }}>No structure available</span>
                 </div>
             </div>;
         }
 
-        const sequenceWrapper = this.getSequenceWrapper();
-
         const params = this.params;
         const values = this.values;
+        const sequenceWrappers = this.getSequenceWrappers(params);
 
         return <div className='msp-sequence'>
             <div className='msp-sequence-select'>
@@ -315,16 +367,34 @@ export class SequenceView extends PluginUIComponent<{ }, SequenceViewState> {
 
                 <span>Sequence of</span>
                 <PureSelectControl title={`[Structure] ${PD.optionLabel(params.structure, values.structure)}`} param={params.structure} name='structure' value={values.structure} onChange={this.setParamProps} />
-                <PureSelectControl title={`[Entity] ${PD.optionLabel(params.entity, values.entity)}`} param={params.entity} name='entity' value={values.entity} onChange={this.setParamProps} />
-                <PureSelectControl title={`[Chain] ${PD.optionLabel(params.chain, values.chain)}`} param={params.chain} name='chain' value={values.chain} onChange={this.setParamProps} />
+                <PureSelectControl title={`[Mode]`} param={SequenceViewModeParam} name='mode' value={values.mode} onChange={this.setParamProps} />
+                {values.mode === 'single' && <PureSelectControl title={`[Entity] ${PD.optionLabel(params.entity, values.entity)}`} param={params.entity} name='entity' value={values.entity} onChange={this.setParamProps} />}
+                {values.mode === 'single' && <PureSelectControl title={`[Chain] ${PD.optionLabel(params.chain, values.chain)}`} param={params.chain} name='chain' value={values.chain} onChange={this.setParamProps} />}
                 {params.operator.options.length > 1 && <>
                     <PureSelectControl title={`[Instance] ${PD.optionLabel(params.operator, values.operator)}`} param={params.operator} name='operator' value={values.operator} onChange={this.setParamProps} />
                 </>}
             </div>
 
-            {typeof sequenceWrapper === 'string'
-                ? <div className='msp-sequence-wrapper msp-sequence-wrapper-non-empty'>{sequenceWrapper}</div>
-                : <Sequence sequenceWrapper={sequenceWrapper} />}
+            <NonEmptySequenceWrapper>
+                {sequenceWrappers.map((s, i) => {
+                    const elem = typeof s.wrapper === 'string'
+                        ? <div key={i} className='msp-sequence-wrapper'>{s.wrapper}</div>
+                        : <Sequence key={i} sequenceWrapper={s.wrapper} />;
+
+                    if (values.mode === 'single') return elem;
+
+                    return <>
+                        <div className='msp-sequence-chain-label'>{s.label}</div>
+                        {elem}
+                    </>;
+                })}
+            </NonEmptySequenceWrapper>
         </div>;
     }
+}
+
+function NonEmptySequenceWrapper({ children }: { children: React.ReactNode }) {
+    return <div className='msp-sequence-wrapper-non-empty'>
+        {children}
+    </div>;
 }

+ 2 - 2
src/mol-plugin-ui/sequence/sequence.tsx

@@ -19,7 +19,7 @@ import { Representation } from '../../mol-repr/representation';
 type SequenceProps = {
     sequenceWrapper: SequenceWrapper.Any,
     sequenceNumberPeriod?: number,
-    hideSequenceNumbers?: boolean
+    hideSequenceNumbers?: boolean,
 }
 
 /** Note, if this is changed, the CSS for `msp-sequence-number` needs adjustment too */
@@ -292,7 +292,7 @@ export class Sequence<P extends SequenceProps> extends PluginUIComponent<P> {
         this.updateMarker();
 
         return <div
-            className='msp-sequence-wrapper msp-sequence-wrapper-non-empty'
+            className='msp-sequence-wrapper'
             onContextMenu={this.contextMenu}
             onMouseDown={this.mouseDown}
             onMouseUp={this.mouseUp}

+ 25 - 7
src/mol-plugin-ui/skin/base/components/sequence.scss

@@ -43,22 +43,32 @@ $sequence-select-height: 24px;
     // use $control-spacing for top to have space for sequence numebrs
     padding: $control-spacing $control-spacing $info-vertical-padding $control-spacing;
     user-select: none;
+}
+
+.msp-sequence-wrapper-non-empty {
+    font-size: 85%;
+    line-height: 180%;
+    font-family: "Courier New", monospace;
+    background: $msp-form-control-background;
+
     width: 100%;
     overflow-y: auto;
     overflow-x: hidden;
     position: absolute;
+    top: $sequence-select-height + 1px;
     left: 0;
-    top: 0;
     bottom: 0;
     right: 0;
 }
 
-.msp-sequence-wrapper-non-empty {
-    font-size: 85%;
-    line-height: 180%;
-    font-family: "Courier New", monospace;
-    background: $msp-form-control-background;
-    top: $sequence-select-height + 1px;
+.msp-sequence-chain-label {
+    margin-left: $control-spacing;
+    margin-top: $control-spacing;
+    user-select: none;
+    color: $sequence-number-color;
+    font-size: 90%;
+    line-height: 90%;
+    padding-left: 0.2em;
 }
 
 .msp-sequence-wrapper {
@@ -74,6 +84,14 @@ $sequence-select-height: 24px;
         margin: 0em 0.2em 0em 0em;
     }
 
+    .msp-sequence-label {
+        color: $sequence-number-color;
+        font-size: 90%;
+        line-height: 90%;
+        padding-bottom: 1em;
+        padding-left: 0.2em;
+    }
+    
     .msp-sequence-number {
         color: $sequence-number-color;
         word-break: keep-all;

+ 21 - 2
src/mol-repr/structure/complex-visual.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 Alexander Rose <alexander.rose@weirdbyte.de>
  */
@@ -31,8 +31,9 @@ import { Text } from '../../mol-geo/geometry/text/text';
 import { SizeTheme } from '../../mol-theme/size';
 import { DirectVolume } from '../../mol-geo/geometry/direct-volume/direct-volume';
 import { createMarkers } from '../../mol-geo/geometry/marker-data';
-import { StructureParams, StructureMeshParams, StructureTextParams, StructureDirectVolumeParams, StructureLinesParams, StructureCylindersParams } from './params';
+import { StructureParams, StructureMeshParams, StructureTextParams, StructureDirectVolumeParams, StructureLinesParams, StructureCylindersParams, StructureTextureMeshParams } from './params';
 import { Clipping } from '../../mol-theme/clipping';
+import { TextureMesh } from '../../mol-geo/geometry/texture-mesh/texture-mesh';
 
 export interface  ComplexVisual<P extends StructureParams> extends Visual<Structure, P> { }
 
@@ -344,4 +345,22 @@ export function ComplexDirectVolumeVisual<P extends ComplexDirectVolumeParams>(b
         },
         geometryUtils: DirectVolume.Utils
     }, materialId);
+}
+
+// texture-mesh
+
+export const ComplexTextureMeshParams = { ...StructureTextureMeshParams, ...StructureParams };
+export type ComplexTextureMeshParams = typeof ComplexTextureMeshParams
+
+export interface ComplexTextureMeshVisualBuilder<P extends ComplexTextureMeshParams> extends ComplexVisualBuilder<P, TextureMesh> { }
+
+export function ComplexTextureMeshVisual<P extends ComplexTextureMeshParams>(builder: ComplexTextureMeshVisualBuilder<P>, materialId: number): ComplexVisual<P> {
+    return ComplexVisual<TextureMesh, P>({
+        ...builder,
+        setUpdateState: (state: VisualUpdateState, newProps: PD.Values<P>, currentProps: PD.Values<P>, newTheme: Theme, currentTheme: Theme, newStructure: Structure, currentStructure: Structure) => {
+            builder.setUpdateState(state, newProps, currentProps, newTheme, currentTheme, newStructure, currentStructure);
+            if (!SizeTheme.areEqual(newTheme.size, currentTheme.size)) state.createGeometry = true;
+        },
+        geometryUtils: TextureMesh.Utils
+    }, materialId);
 }

+ 3 - 1
src/mol-repr/structure/representation/gaussian-surface.ts

@@ -1,5 +1,5 @@
 /**
- * Copyright (c) 2018-2019 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 Alexander Rose <alexander.rose@weirdbyte.de>
  */
@@ -17,6 +17,8 @@ const GaussianSurfaceVisuals = {
     'gaussian-surface-mesh': (ctx: RepresentationContext, getParams: RepresentationParamsGetter<Structure, GaussianSurfaceMeshParams>) => UnitsRepresentation('Gaussian surface mesh', ctx, getParams, GaussianSurfaceMeshVisual),
     'structure-gaussian-surface-mesh': (ctx: RepresentationContext, getParams: RepresentationParamsGetter<Structure, StructureGaussianSurfaceMeshParams>) => ComplexRepresentation('Structure-Gaussian surface mesh', ctx, getParams, StructureGaussianSurfaceMeshVisual),
     'gaussian-surface-texture-mesh': (ctx: RepresentationContext, getParams: RepresentationParamsGetter<Structure, GaussianSurfaceMeshParams>) => UnitsRepresentation('Gaussian surface texture-mesh', ctx, getParams, GaussianSurfaceTextureMeshVisual),
+    // TODO: don't enable yet as it breaks state sessions
+    // 'structure-gaussian-surface-texture-mesh': (ctx: RepresentationContext, getParams: RepresentationParamsGetter<Structure, StructureGaussianSurfaceMeshParams>) => ComplexRepresentation('Structure-Gaussian surface texture-mesh', ctx, getParams, StructureGaussianSurfaceTextureMeshVisual),
     'gaussian-surface-wireframe': (ctx: RepresentationContext, getParams: RepresentationParamsGetter<Structure, GaussianWireframeParams>) => UnitsRepresentation('Gaussian surface wireframe', ctx, getParams, GaussianWireframeVisual),
 };
 

+ 93 - 19
src/mol-repr/structure/visual/gaussian-surface-mesh.ts

@@ -1,12 +1,12 @@
 /**
- * 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 Alexander Rose <alexander.rose@weirdbyte.de>
  */
 
 import { ParamDefinition as PD } from '../../../mol-util/param-definition';
 import { UnitsMeshParams, UnitsTextureMeshParams, UnitsVisual, UnitsMeshVisual, UnitsTextureMeshVisual } from '../units-visual';
-import { GaussianDensityParams, computeUnitGaussianDensity, GaussianDensityTextureProps, computeUnitGaussianDensityTexture2d, GaussianDensityProps, computeStructureGaussianDensity } from './util/gaussian';
+import { GaussianDensityParams, computeUnitGaussianDensity, GaussianDensityTextureProps, computeUnitGaussianDensityTexture2d, GaussianDensityProps, computeStructureGaussianDensity, computeStructureGaussianDensityTexture2d } from './util/gaussian';
 import { VisualContext } from '../../visual';
 import { Unit, Structure } from '../../../mol-model/structure';
 import { Theme } from '../../../mol-theme/theme';
@@ -19,7 +19,7 @@ import { calcActiveVoxels } from '../../../mol-gl/compute/marching-cubes/active-
 import { createHistogramPyramid } from '../../../mol-gl/compute/histogram-pyramid/reduction';
 import { createIsosurfaceBuffers } from '../../../mol-gl/compute/marching-cubes/isosurface';
 import { Sphere3D } from '../../../mol-math/geometry';
-import { ComplexVisual, ComplexMeshParams, ComplexMeshVisual } from '../complex-visual';
+import { ComplexVisual, ComplexMeshParams, ComplexMeshVisual, ComplexTextureMeshVisual } from '../complex-visual';
 import { getUnitExtraRadius, getStructureExtraRadius } from './util/common';
 
 export const GaussianSurfaceMeshParams = {
@@ -124,39 +124,47 @@ const GaussianSurfaceName = 'gaussian-surface';
 async function createGaussianSurfaceTextureMesh(ctx: VisualContext, unit: Unit, structure: Structure, theme: Theme, props: GaussianDensityTextureProps, textureMesh?: TextureMesh): Promise<TextureMesh> {
     if (!ctx.webgl) throw new Error('webgl context required to create gaussian surface texture-mesh');
 
-    const { namedTextures, resources, extensions: { colorBufferFloat, textureFloat } } = ctx.webgl;
+    const { namedTextures, resources, extensions: { colorBufferFloat, textureFloat, colorBufferHalfFloat, textureHalfFloat } } = ctx.webgl;
     if (!namedTextures[GaussianSurfaceName]) {
-        namedTextures[GaussianSurfaceName] = colorBufferFloat && textureFloat
-            ? resources.texture('image-float32', 'rgba', 'float', 'linear')
-            : resources.texture('image-uint8', 'rgba', 'ubyte', 'linear');
+        namedTextures[GaussianSurfaceName] = colorBufferHalfFloat && textureHalfFloat
+            ? resources.texture('image-float16', 'rgba', 'fp16', 'linear')
+            : colorBufferFloat && textureFloat
+                ? resources.texture('image-float32', 'rgba', 'float', 'linear')
+                : resources.texture('image-uint8', 'rgba', 'ubyte', 'linear');
     }
 
-    const densityTextureData = await computeUnitGaussianDensityTexture2d(structure, unit, props, ctx.webgl, namedTextures[GaussianSurfaceName]).runInContext(ctx.runtime);
-    // console.log(densityTextureData)
-    // console.log('vertexGroupTexture', readTexture(ctx.webgl, densityTextureData.texture))
-    // ctx.webgl.waitForGpuCommandsCompleteSync()
+    // console.time('computeUnitGaussianDensityTexture2d');
+    const densityTextureData = await computeUnitGaussianDensityTexture2d(structure, unit, true, props, ctx.webgl, namedTextures[GaussianSurfaceName]).runInContext(ctx.runtime);
+    // console.log(densityTextureData);
+    // console.log('vertexGroupTexture', readTexture(ctx.webgl, densityTextureData.texture));
+    // ctx.webgl.waitForGpuCommandsCompleteSync();
+    // console.timeEnd('computeUnitGaussianDensityTexture2d');
 
     const isoLevel = Math.exp(-props.smoothness) / densityTextureData.radiusFactor;
 
+    // console.time('calcActiveVoxels');
     const activeVoxelsTex = calcActiveVoxels(ctx.webgl, densityTextureData.texture, densityTextureData.gridDim, densityTextureData.gridTexDim, isoLevel, densityTextureData.gridTexScale);
-    // ctx.webgl.waitForGpuCommandsCompleteSync()
+    // ctx.webgl.waitForGpuCommandsCompleteSync();
+    // console.timeEnd('calcActiveVoxels');
 
-    const compacted = createHistogramPyramid(ctx.webgl, activeVoxelsTex, densityTextureData.gridTexScale);
-    // ctx.webgl.waitForGpuCommandsCompleteSync()
+    // console.time('createHistogramPyramid');
+    const compacted = createHistogramPyramid(ctx.webgl, activeVoxelsTex, densityTextureData.gridTexScale, densityTextureData.gridTexDim);
+    // ctx.webgl.waitForGpuCommandsCompleteSync();
+    // console.timeEnd('createHistogramPyramid');
 
+    // console.time('createIsosurfaceBuffers');
     const gv = createIsosurfaceBuffers(ctx.webgl, activeVoxelsTex, densityTextureData.texture, compacted, densityTextureData.gridDim, densityTextureData.gridTexDim, densityTextureData.transform, isoLevel, textureMesh ? textureMesh.vertexGroupTexture.ref.value : undefined, textureMesh ? textureMesh.normalTexture.ref.value : undefined);
-    // ctx.webgl.waitForGpuCommandsCompleteSync()
+    // ctx.webgl.waitForGpuCommandsCompleteSync();
+    // console.timeEnd('createIsosurfaceBuffers');
 
-    // const boundingSphere = Sphere3D.zero()
-    // Sphere3D.addVec3(boundingSphere, boundingSphere, densityTextureData.gridDimension)
-    const boundingSphere = Sphere3D.fromBox3D(Sphere3D(), densityTextureData.bbox);
+    const boundingSphere = Sphere3D.expand(Sphere3D(), structure.boundary.sphere, props.radiusOffset + getStructureExtraRadius(structure));
     const surface = TextureMesh.create(gv.vertexCount, 1, gv.vertexGroupTexture, gv.normalTexture, boundingSphere, textureMesh);
     // console.log({
     //     renderables: ctx.webgl.namedComputeRenderables,
     //     framebuffers: ctx.webgl.namedFramebuffers,
     //     textures: ctx.webgl.namedTextures,
     // });
-    // ctx.webgl.waitForGpuCommandsCompleteSync()
+    // ctx.webgl.waitForGpuCommandsCompleteSync();
     return surface;
 }
 
@@ -176,4 +184,70 @@ export function GaussianSurfaceTextureMeshVisual(materialId: number): UnitsVisua
             if (newProps.includeParent !== currentProps.includeParent) state.createGeometry = true;
         }
     }, materialId);
+}
+
+//
+
+async function createStructureGaussianSurfaceTextureMesh(ctx: VisualContext, structure: Structure, theme: Theme, props: GaussianDensityTextureProps, textureMesh?: TextureMesh): Promise<TextureMesh> {
+    if (!ctx.webgl) throw new Error('webgl context required to create structure gaussian surface texture-mesh');
+
+    const { namedTextures, resources, extensions: { colorBufferFloat, textureFloat, colorBufferHalfFloat, textureHalfFloat } } = ctx.webgl;
+    if (!namedTextures[GaussianSurfaceName]) {
+        namedTextures[GaussianSurfaceName] = colorBufferHalfFloat && textureHalfFloat
+            ? resources.texture('image-float16', 'rgba', 'fp16', 'linear')
+            : colorBufferFloat && textureFloat
+                ? resources.texture('image-float32', 'rgba', 'float', 'linear')
+                : resources.texture('image-uint8', 'rgba', 'ubyte', 'linear');
+    }
+
+    // console.time('computeUnitGaussianDensityTexture2d');
+    const densityTextureData = await computeStructureGaussianDensityTexture2d(structure, true, props, ctx.webgl, namedTextures[GaussianSurfaceName]).runInContext(ctx.runtime);
+    // console.log(densityTextureData);
+    // console.log('vertexGroupTexture', readTexture(ctx.webgl, densityTextureData.texture));
+    // ctx.webgl.waitForGpuCommandsCompleteSync();
+    // console.timeEnd('computeUnitGaussianDensityTexture2d');
+
+    const isoLevel = Math.exp(-props.smoothness) / densityTextureData.radiusFactor;
+
+    // console.time('calcActiveVoxels');
+    const activeVoxelsTex = calcActiveVoxels(ctx.webgl, densityTextureData.texture, densityTextureData.gridDim, densityTextureData.gridTexDim, isoLevel, densityTextureData.gridTexScale);
+    // ctx.webgl.waitForGpuCommandsCompleteSync();
+    // console.timeEnd('calcActiveVoxels');
+
+    // console.time('createHistogramPyramid');
+    const compacted = createHistogramPyramid(ctx.webgl, activeVoxelsTex, densityTextureData.gridTexScale, densityTextureData.gridTexDim);
+    // ctx.webgl.waitForGpuCommandsCompleteSync();
+    // console.timeEnd('createHistogramPyramid');
+
+    // console.time('createIsosurfaceBuffers');
+    const gv = createIsosurfaceBuffers(ctx.webgl, activeVoxelsTex, densityTextureData.texture, compacted, densityTextureData.gridDim, densityTextureData.gridTexDim, densityTextureData.transform, isoLevel, textureMesh ? textureMesh.vertexGroupTexture.ref.value : undefined, textureMesh ? textureMesh.normalTexture.ref.value : undefined);
+    // ctx.webgl.waitForGpuCommandsCompleteSync();
+    // console.timeEnd('createIsosurfaceBuffers');
+
+    const boundingSphere = Sphere3D.expand(Sphere3D(), structure.boundary.sphere, props.radiusOffset + getStructureExtraRadius(structure));
+    const surface = TextureMesh.create(gv.vertexCount, 1, gv.vertexGroupTexture, gv.normalTexture, boundingSphere, textureMesh);
+    // console.log({
+    //     renderables: ctx.webgl.namedComputeRenderables,
+    //     framebuffers: ctx.webgl.namedFramebuffers,
+    //     textures: ctx.webgl.namedTextures,
+    // });
+    // ctx.webgl.waitForGpuCommandsCompleteSync();
+    return surface;
+}
+
+export function StructureGaussianSurfaceTextureMeshVisual(materialId: number): ComplexVisual<StructureGaussianSurfaceMeshParams> {
+    return ComplexTextureMeshVisual<StructureGaussianSurfaceMeshParams>({
+        defaultProps: PD.getDefaultValues(StructureGaussianSurfaceMeshParams),
+        createGeometry: createStructureGaussianSurfaceTextureMesh,
+        createLocationIterator: ElementIterator.fromStructure,
+        getLoci: getSerialElementLoci,
+        eachLocation: eachSerialElement,
+        setUpdateState: (state: VisualUpdateState, newProps: PD.Values<StructureGaussianSurfaceMeshParams>, currentProps: PD.Values<StructureGaussianSurfaceMeshParams>) => {
+            if (newProps.resolution !== currentProps.resolution) state.createGeometry = true;
+            if (newProps.radiusOffset !== currentProps.radiusOffset) state.createGeometry = true;
+            if (newProps.smoothness !== currentProps.smoothness) state.createGeometry = true;
+            if (newProps.ignoreHydrogens !== currentProps.ignoreHydrogens) state.createGeometry = true;
+            if (newProps.traceOnly !== currentProps.traceOnly) state.createGeometry = true;
+        }
+    }, materialId);
 }

+ 1 - 1
src/mol-repr/structure/visual/util/common.ts

@@ -159,7 +159,7 @@ export function getUnitConformationAndRadius(structure: Structure, unit: Unit, p
     let indices: SortedArray<ElementIndex>;
     let id: AssignableArrayLike<number>;
 
-    if (ignoreHydrogens || (includeParent && rootUnit !== unit)) {
+    if (ignoreHydrogens || traceOnly || (includeParent && rootUnit !== unit)) {
         const _indices = [];
         const _id = [];
         for (let i = 0, il = elements.length; i < il; ++i) {

+ 11 - 2
src/mol-repr/structure/visual/util/gaussian.ts

@@ -54,12 +54,12 @@ export function computeUnitGaussianDensityTexture(structure: Structure, unit: Un
     });
 }
 
-export function computeUnitGaussianDensityTexture2d(structure: Structure, unit: Unit, props: GaussianDensityTextureProps, webgl: WebGLContext, texture?: Texture) {
+export function computeUnitGaussianDensityTexture2d(structure: Structure, unit: Unit, powerOfTwo: boolean, props: GaussianDensityTextureProps, webgl: WebGLContext, texture?: Texture) {
     const { box } = unit.lookup3d.boundary;
     const p = ensureReasonableResolution(box, props);
     const { position, radius } = getUnitConformationAndRadius(structure, unit, p);
     return Task.create('Gaussian Density', async ctx => {
-        return GaussianDensityTexture2d(webgl, position, box, radius, p, texture);
+        return GaussianDensityTexture2d(webgl, position, box, radius, powerOfTwo, p, texture);
     });
 }
 
@@ -82,3 +82,12 @@ export function computeStructureGaussianDensityTexture(structure: Structure, pro
         return GaussianDensityTexture(webgl, position, box, radius, p, texture);
     });
 }
+
+export function computeStructureGaussianDensityTexture2d(structure: Structure, powerOfTwo: boolean, props: GaussianDensityTextureProps, webgl: WebGLContext, texture?: Texture) {
+    const { box } = structure.lookup3d.boundary;
+    const p = ensureReasonableResolution(box, props);
+    const { position, radius } = getStructureConformationAndRadius(structure, props.ignoreHydrogens, props.traceOnly);
+    return Task.create('Gaussian Density', async ctx => {
+        return GaussianDensityTexture2d(webgl, position, box, radius, powerOfTwo, p, texture);
+    });
+}

+ 3 - 0
src/servers/model/CHANGELOG.md

@@ -1,3 +1,6 @@
+# 0.9.6
+* optional download parameter
+
 # 0.9.5
 * Support molstar_global_model_transform_info category.
 

+ 2 - 1
src/servers/model/server/api-web-multiple.ts

@@ -18,7 +18,8 @@ export interface MultipleQueryEntry<Name extends QueryName = QueryName> {
 export interface MultipleQuerySpec {
     queries: MultipleQueryEntry[],
     encoding?: Encoding,
-    asTarGz?: boolean
+    asTarGz?: boolean,
+    download?: boolean
 }
 
 export function getMultiQuerySpecFilename() {

+ 8 - 7
src/servers/model/server/api-web.ts

@@ -11,7 +11,7 @@ import * as bodyParser from 'body-parser';
 import { ModelServerConfig as Config, ModelServerConfig, mapSourceAndIdToFilename } from '../config';
 import { ConsoleLogger } from '../../../mol-util/console-logger';
 import { resolveJob } from './query';
-import { JobManager, JobEntry } from './jobs';
+import { JobManager, JobEntry, ResultWriterParams } from './jobs';
 import { UUID } from '../../../mol-util';
 import { QueryDefinition, normalizeRestQueryParams, normalizeRestCommonParams, QueryList } from './api';
 import { getApiSchema, shortcutIconLink } from './api-schema';
@@ -45,17 +45,18 @@ async function processNextJob() {
     }
 }
 
-export function createResultWriter(response: express.Response, encoding: string, entryId?: string, queryName?: string) {
-    const filenameBase = entryId && queryName
-        ? `${entryId}_${splitCamelCase(queryName.replace(/\s/g, '_'), '-').toLowerCase()}`
+export function createResultWriter(response: express.Response, params: ResultWriterParams) {
+    const filenameBase = params.entryId && params.queryName
+        ? `${params.entryId}_${splitCamelCase(params.queryName.replace(/\s/g, '_'), '-').toLowerCase()}`
         : `result`;
-    return new SimpleResponseResultWriter(`${filenameBase}.${encoding}`, response, encoding === 'bcif');
+    return new SimpleResponseResultWriter(`${filenameBase}.${params.encoding}`, response, params.encoding === 'bcif', params.download);
 }
 
 function mapQuery(app: express.Express, queryName: string, queryDefinition: QueryDefinition) {
     function createJob(queryParams: any, req: express.Request, res: express.Response) {
         const entryId = req.params.id;
         const commonParams = normalizeRestCommonParams(req.query);
+        const resultWriterParams = { encoding: commonParams.encoding!, download: !!commonParams.download, entryId, queryName };
         const jobId = JobManager.add({
             entries: [JobEntry({
                 sourceId: commonParams.data_source || ModelServerConfig.defaultSource,
@@ -66,7 +67,7 @@ function mapQuery(app: express.Express, queryName: string, queryDefinition: Quer
                 copyAllCategories: !!commonParams.copy_all_categories,
                 transform: commonParams.transform
             })],
-            writer: createResultWriter(res, commonParams.encoding!, entryId, queryName),
+            writer: createResultWriter(res, resultWriterParams),
             options: { binary: commonParams.encoding === 'bcif', encoding: commonParams.encoding }
         });
         responseMap.set(jobId, res);
@@ -122,7 +123,7 @@ function serveStatic(req: express.Request, res: express.Response) {
 function createMultiJob(spec: MultipleQuerySpec, res: express.Response) {
     const writer = spec.asTarGz
         ? new TarballResponseResultWriter(getMultiQuerySpecFilename(), res)
-        : createResultWriter(res, spec.encoding!);
+        : createResultWriter(res, { encoding: spec.encoding!, download: !!spec.download });
 
     if (spec.queries.length > ModelServerConfig.maxQueryManyQueries) {
         writer.doError(400, `query-many queries limit (${ModelServerConfig.maxQueryManyQueries}) exceeded.`);

+ 14 - 4
src/servers/model/server/api.ts

@@ -48,7 +48,8 @@ export const CommonQueryParamsInfo: QueryParamInfo[] = [
     { name: 'encoding', type: QueryParamType.String, defaultValue: 'cif', description: `Determines the output encoding (text based 'CIF' or binary 'BCIF'). Ligands can also be exported as 'SDF', 'MOL', or 'MOL2'.`, supportedValues: ['cif', 'bcif', 'sdf', 'mol', 'mol2'] },
     { name: 'copy_all_categories', type: QueryParamType.Boolean, defaultValue: false, description: 'If true, copy all categories from the input file.' },
     { name: 'data_source', type: QueryParamType.String, defaultValue: '', description: 'Allows to control how the provided data source ID maps to input file (as specified by the server instance config).' },
-    { name: 'transform', type: QueryParamType.String, description: `Transformation to apply to coordinates in '_atom_site'. Accepts a 4x4 transformation matrix, provided as array of 16 float values.` }
+    { name: 'transform', type: QueryParamType.String, description: `Transformation to apply to coordinates in '_atom_site'. Accepts a 4x4 transformation matrix, provided as array of 16 float values.` },
+    { name: 'download', type: QueryParamType.Boolean, defaultValue: false, description: 'If true, browser will download text files.' }
 ];
 
 export type Encoding = 'cif' | 'bcif' | 'sdf' | 'mol' | 'mol2';
@@ -57,7 +58,8 @@ export interface CommonQueryParamsInfo {
     encoding?: Encoding,
     copy_all_categories?: boolean
     data_source?: string,
-    transform?: Mat4
+    transform?: Mat4,
+    download?: boolean
 }
 
 export const AtomSiteSchemaElement = {
@@ -290,12 +292,20 @@ export function normalizeRestCommonParams(params: any): CommonQueryParamsInfo {
     return {
         model_nums: params.model_nums ? ('' + params.model_nums).split(',').map(n => n.trim()).filter(n => !!n).map(n => +n) : void 0,
         data_source: params.data_source,
-        copy_all_categories: Boolean(params.copy_all_categories),
+        copy_all_categories: isTrue(params.copy_all_categories),
         encoding: mapEncoding(('' + params.encoding).toLocaleLowerCase()),
-        transform: params.transform ? ('' + params.transform).split(',').map(n => n.trim()).map(n => +n) as Mat4 : Mat4.identity()
+        transform: params.transform ? ('' + params.transform).split(',').map(n => n.trim()).map(n => +n) as Mat4 : Mat4.identity(),
+        download: isTrue(params.download)
     };
 }
 
+function isTrue(val: any): boolean {
+    const b = Boolean(val);
+    if (!b) return false;
+    if (typeof val === 'string') return val !== '0' && val.toLowerCase() !== 'false';
+    return b;
+}
+
 function mapEncoding(value: string) {
     switch (value) {
         case 'bcif':

+ 7 - 0
src/servers/model/server/jobs.ts

@@ -56,6 +56,13 @@ interface JobEntryDefinition<Name extends QueryName> {
     transform?: Mat4
 }
 
+export interface ResultWriterParams {
+    encoding: Encoding,
+    download: boolean,
+    entryId?: string,
+    queryName?: string
+}
+
 export function JobEntry<Name extends QueryName>(definition: JobEntryDefinition<Name>): JobEntry {
     const queryDefinition = getQueryByName(definition.queryName);
     if (!queryDefinition) throw new Error(`Query '${definition.queryName}' is not supported.`);

+ 3 - 2
src/servers/model/utils/writer.ts

@@ -48,10 +48,11 @@ export class SimpleResponseResultWriter implements WebResutlWriter {
         this.headerWritten = true;
 
         this.res.writeHead(200, {
+            // TODO there seems to be a bug in swagger-ui - front-end will freeze for cif delivered as text/plain (forcing binary is a hack to circumvent this)
             'Content-Type': this.isBinary ? 'application/octet-stream' : 'text/plain; charset=utf-8',
             'Access-Control-Allow-Origin': '*',
             'Access-Control-Allow-Headers': 'X-Requested-With',
-            'Content-Disposition': `inline; filename="${this.fn}"`
+            'Content-Disposition': `${this.isDownload ? 'attachment' : 'inline'}; filename="${this.fn}"`
         });
     }
 
@@ -71,7 +72,7 @@ export class SimpleResponseResultWriter implements WebResutlWriter {
         this.ended = true;
     }
 
-    constructor(private fn: string, private res: express.Response, private isBinary: boolean) {
+    constructor(private fn: string, private res: express.Response, private isBinary: boolean, private isDownload: boolean) {
 
     }
 }

+ 1 - 1
src/servers/model/version.ts

@@ -4,4 +4,4 @@
  * @author David Sehnal <david.sehnal@gmail.com>
  */
 
-export default '0.9.5';
+export default '0.9.6';

+ 2 - 2
src/tests/browser/marching-cubes.ts

@@ -68,7 +68,7 @@ async function init() {
         console.timeEnd('gpu mc active2');
 
         console.time('gpu mc pyramid2');
-        const compacted2 = createHistogramPyramid(webgl, activeVoxelsTex2, densityTextureData2.gridTexScale);
+        const compacted2 = createHistogramPyramid(webgl, activeVoxelsTex2, densityTextureData2.gridTexScale, densityTextureData2.gridTexDim);
         webgl.waitForGpuCommandsCompleteSync();
         console.timeEnd('gpu mc pyramid2');
 
@@ -91,7 +91,7 @@ async function init() {
     console.timeEnd('gpu mc active');
 
     console.time('gpu mc pyramid');
-    const compacted = createHistogramPyramid(webgl, activeVoxelsTex, densityTextureData.gridTexScale);
+    const compacted = createHistogramPyramid(webgl, activeVoxelsTex, densityTextureData.gridTexScale, densityTextureData.gridTexDim);
     webgl.waitForGpuCommandsCompleteSync();
     console.timeEnd('gpu mc pyramid');