Explorar el Código

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

Alexander Rose hace 4 años
padre
commit
698f7e16bd

+ 13 - 1
CHANGELOG.md

@@ -5,9 +5,21 @@ Note that since we don't clearly distinguish between a public and private interf
 
 ## [Unreleased]
 
+- Ability to pass ``Canvas3DContext`` to ``PluginContext.fromCanvas``.
+- Relative frame support for ``Canvas3D`` viewport.
+- Fix bug in screenshot copy UI.
+- Add ability to select residues from a list of identifiers to the Selection UI.
+- Fix SSAO bugs when used with ``Canvas3D`` viewport.
+- Support for  full pausing (no draw) rendering: ``Canvas3D.pause(true)``.
+- Add `MeshBuilder.addMesh`.
+- Add `Torus` primitive.
+
+## [v2.0.4] - 2021-04-20
+
 - [WIP] Mesh export extension
-- ``Structure.eachAtomicHierarchyElement``
+- ``Structure.eachAtomicHierarchyElement`` (#161)
 - Fixed reading multi-line values in SDF format
+- Fixed Measurements UI labels (#166)
 
 ## [v2.0.3] - 2021-04-09
 ### Added

+ 2 - 2
package-lock.json

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

+ 1 - 1
package.json

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

+ 8 - 3
src/mol-canvas3d/camera.ts

@@ -87,7 +87,12 @@ class Camera implements ICamera {
 
         if (changed) {
             Mat4.mul(this.projectionView, this.projection, this.view);
-            Mat4.invert(this.inverseProjectionView, this.projectionView);
+            if (!Mat4.tryInvert(this.inverseProjectionView, this.projectionView)) {
+                Mat4.copy(this.view, this.prevView);
+                Mat4.copy(this.projection, this.prevProjection);
+                Mat4.mul(this.projectionView, this.projection, this.view);
+                return false;
+            }
 
             Mat4.copy(this.prevView, this.view);
             Mat4.copy(this.prevProjection, this.projection);
@@ -229,8 +234,8 @@ namespace Camera {
             up: Vec3.create(0, 1, 0),
             target: Vec3.create(0, 0, 0),
 
-            radius: 0,
-            radiusMax: 0,
+            radius: 10,
+            radiusMax: 10,
             fog: 50,
             clipFar: true
         };

+ 3 - 0
src/mol-canvas3d/camera/transition.ts

@@ -39,6 +39,9 @@ class CameraTransitionManager {
             this._target.radius = this._target.radiusMax;
         }
 
+        if (this._target.radius < 0.01) this._target.radius = 0.01;
+        if (this._target.radiusMax < 0.01) this._target.radiusMax = 0.01;
+
         if (!this.inTransition && durationMs <= 0 || (typeof to.mode !== 'undefined' && to.mode !== this.camera.state.mode)) {
             this.finish(this._target);
             return;

+ 31 - 8
src/mol-canvas3d/canvas3d.ts

@@ -61,11 +61,17 @@ export const Canvas3DParams = {
     }, { pivot: 'radius' }),
     viewport: PD.MappedStatic('canvas', {
         canvas: PD.Group({}),
-        custom: PD.Group({
+        'static-frame': PD.Group({
             x: PD.Numeric(0),
             y: PD.Numeric(0),
             width: PD.Numeric(128),
             height: PD.Numeric(128)
+        }),
+        'relative-frame': PD.Group({
+            x: PD.Numeric(0.33, { min: 0, max: 1, step: 0.01 }),
+            y: PD.Numeric(0.33, { min: 0, max: 1, step: 0.01 }),
+            width: PD.Numeric(0.5, { min: 0.01, max: 1, step: 0.01 }),
+            height: PD.Numeric(0.5, { min: 0.01, max: 1, step: 0.01 })
         })
     }),
 
@@ -100,7 +106,7 @@ interface Canvas3DContext {
 }
 
 namespace Canvas3DContext {
-    const DefaultAttribs = {
+    export const DefaultAttribs = {
         /** true by default to avoid issues with Safari (Jan 2021) */
         antialias: true,
         /** true to support multiple Canvas3D objects with a single context */
@@ -201,7 +207,7 @@ interface Canvas3D {
      */
     commit(isSynchronous?: boolean): void
     /**
-     * Funcion for external "animation" control
+     * Function for external "animation" control
      * Calls commit.
      */
     tick(t: now.Timestamp, options?: { isSynchronous?: boolean, manualDraw?: boolean }): void
@@ -214,7 +220,11 @@ interface Canvas3D {
     /** Reset the timers, used by "animate" */
     resetTime(t: number): void
     animate(): void
-    pause(): void
+    /**
+     * Pause animation loop and optionally any rendering
+     * @param noDraw pause any rendering
+     */
+    pause(noDraw?: boolean): void
     identify(x: number, y: number): PickData | undefined
     mark(loci: Representation.Loci, action: MarkerAction): void
     getLoci(pickingId: PickingId | undefined): Representation.Loci
@@ -386,8 +396,10 @@ namespace Canvas3D {
         let forceNextDraw = false;
         let forceDrawAfterAllCommited = false;
         let currentTime = 0;
+        let drawPaused = false;
 
         function draw(force?: boolean) {
+            if (drawPaused) return;
             if (render(!!force || forceNextDraw) && notifyDidDraw) {
                 didDraw.next(now() - startTime as now.Timestamp);
             }
@@ -429,11 +441,13 @@ namespace Canvas3D {
         }
 
         function animate() {
+            drawPaused = false;
             controls.start(now());
             if (animationFrameHandle === 0) _animate();
         }
 
-        function pause() {
+        function pause(noDraw = false) {
+            drawPaused = noDraw;
             cancelAnimationFrame(animationFrameHandle);
             animationFrameHandle = 0;
         }
@@ -805,12 +819,21 @@ namespace Canvas3D {
                 y = 0;
                 width = gl.drawingBufferWidth;
                 height = gl.drawingBufferHeight;
-            } else {
+            } else if (p.viewport.name === 'static-frame') {
                 x = p.viewport.params.x * webgl.pixelRatio;
-                y = p.viewport.params.y * webgl.pixelRatio;
-                width = p.viewport.params.width * webgl.pixelRatio;
                 height = p.viewport.params.height * webgl.pixelRatio;
+                y = gl.drawingBufferHeight - height - p.viewport.params.y * webgl.pixelRatio;
+                width = p.viewport.params.width * webgl.pixelRatio;
+            } else if (p.viewport.name === 'relative-frame') {
+                x = Math.round(p.viewport.params.x * gl.drawingBufferWidth);
+                height = Math.round(p.viewport.params.height * gl.drawingBufferHeight);
+                y = Math.round(gl.drawingBufferHeight - height - p.viewport.params.y * gl.drawingBufferHeight);
+                width = Math.round(p.viewport.params.width * gl.drawingBufferWidth);
+                // if (x + width >= gl.drawingBufferWidth) width = gl.drawingBufferWidth - x;
+                // if (y + height >= gl.drawingBufferHeight) height = gl.drawingBufferHeight - y - 1;
+                // console.log({ x, y, width, height });
             }
+
         }
 
         function syncViewport() {

+ 2 - 2
src/mol-canvas3d/passes/pick.ts

@@ -128,8 +128,8 @@ export class PickHelper {
         this.pickX = Math.ceil(x * this.pickScale);
         this.pickY = Math.ceil(y * this.pickScale);
 
-        const pickWidth = Math.ceil(width * this.pickScale);
-        const pickHeight = Math.ceil(height * this.pickScale);
+        const pickWidth = Math.floor(width * this.pickScale);
+        const pickHeight = Math.floor(height * this.pickScale);
 
         if (pickWidth !== this.pickWidth || pickHeight !== this.pickHeight) {
             this.pickWidth = pickWidth;

+ 53 - 25
src/mol-canvas3d/passes/postprocessing.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>
  * @author Áron Samuel Kovács <aron.kovacs@mail.muni.cz>
@@ -13,7 +13,7 @@ import { Texture } from '../../mol-gl/webgl/texture';
 import { ValueCell } from '../../mol-util';
 import { createComputeRenderItem } from '../../mol-gl/webgl/render-item';
 import { createComputeRenderable, ComputeRenderable } from '../../mol-gl/renderable';
-import { Mat4, Vec2, Vec3 } from '../../mol-math/linear-algebra';
+import { Mat4, Vec2, Vec3, Vec4 } from '../../mol-math/linear-algebra';
 import { ParamDefinition as PD } from '../../mol-util/param-definition';
 import { RenderTarget } from '../../mol-gl/webgl/render-target';
 import { DrawPass } from './draw';
@@ -70,6 +70,7 @@ const SsaoSchema = {
 
     uProjection: UniformSpec('m4'),
     uInvProjection: UniformSpec('m4'),
+    uBounds: UniformSpec('v4'),
 
     uTexSize: UniformSpec('v2'),
 
@@ -89,6 +90,7 @@ function getSsaoRenderable(ctx: WebGLContext, depthTexture: Texture): SsaoRender
 
         uProjection: ValueCell.create(Mat4.identity()),
         uInvProjection: ValueCell.create(Mat4.identity()),
+        uBounds: ValueCell.create(Vec4()),
 
         uTexSize: ValueCell.create(Vec2.create(ctx.gl.drawingBufferWidth, ctx.gl.drawingBufferHeight)),
 
@@ -118,6 +120,7 @@ const SsaoBlurSchema = {
 
     uNear: UniformSpec('f'),
     uFar: UniformSpec('f'),
+    uBounds: UniformSpec('v4'),
     dOrthographic: DefineSpec('number'),
 };
 
@@ -139,6 +142,7 @@ function getSsaoBlurRenderable(ctx: WebGLContext, ssaoDepthTexture: Texture, dir
 
         uNear: ValueCell.create(0.0),
         uFar: ValueCell.create(10000.0),
+        uBounds: ValueCell.create(Vec4()),
         dOrthographic: ValueCell.create(0),
     };
 
@@ -286,10 +290,14 @@ export class PostprocessingPass {
 
     private readonly renderable: PostprocessingRenderable
 
-    private scale: number
+    private ssaoScale: number
+    private calcSsaoScale() {
+        // downscale ssao for high pixel-ratios
+        return Math.min(1, 1 / this.webgl.pixelRatio);
+    }
 
     constructor(private webgl: WebGLContext, drawPass: DrawPass) {
-        this.scale = 1 / this.webgl.pixelRatio;
+        this.ssaoScale = this.calcSsaoScale();
 
         const { colorTarget, depthTexture } = drawPass;
         const width = colorTarget.getWidth();
@@ -298,7 +306,7 @@ export class PostprocessingPass {
         this.nSamples = 1;
         this.blurKernelSize = 1;
 
-        this.target = webgl.createRenderTarget(width, height, false, 'uint8', 'linear');
+        this.target = webgl.createRenderTarget(width, height, false, 'uint8', 'nearest');
 
         this.outlinesTarget = webgl.createRenderTarget(width, height, false);
         this.outlinesRenderable = getOutlinesRenderable(webgl, depthTexture);
@@ -317,14 +325,14 @@ export class PostprocessingPass {
         this.ssaoBlurFirstPassFramebuffer = webgl.resources.framebuffer();
         this.ssaoBlurSecondPassFramebuffer = webgl.resources.framebuffer();
 
-        const sw = Math.floor(width * this.scale);
-        const sh = Math.floor(height * this.scale);
+        const sw = Math.floor(width * this.ssaoScale);
+        const sh = Math.floor(height * this.ssaoScale);
 
-        this.ssaoDepthTexture = webgl.resources.texture('image-uint8', 'rgba', 'ubyte', 'linear');
+        this.ssaoDepthTexture = webgl.resources.texture('image-uint8', 'rgba', 'ubyte', 'nearest');
         this.ssaoDepthTexture.define(sw, sh);
         this.ssaoDepthTexture.attachFramebuffer(this.ssaoFramebuffer, 'color0');
 
-        this.ssaoDepthBlurProxyTexture = webgl.resources.texture('image-uint8', 'rgba', 'ubyte', 'linear');
+        this.ssaoDepthBlurProxyTexture = webgl.resources.texture('image-uint8', 'rgba', 'ubyte', 'nearest');
         this.ssaoDepthBlurProxyTexture.define(sw, sh);
         this.ssaoDepthBlurProxyTexture.attachFramebuffer(this.ssaoBlurFirstPassFramebuffer, 'color0');
 
@@ -338,9 +346,13 @@ export class PostprocessingPass {
 
     setSize(width: number, height: number) {
         const [w, h] = this.renderable.values.uTexSize.ref.value;
-        if (width !== w || height !== h) {
-            const sw = Math.floor(width * this.scale);
-            const sh = Math.floor(height * this.scale);
+        const ssaoScale = this.calcSsaoScale();
+
+        if (width !== w || height !== h || this.ssaoScale !== ssaoScale) {
+            this.ssaoScale = ssaoScale;
+
+            const sw = Math.floor(width * this.ssaoScale);
+            const sh = Math.floor(height * this.ssaoScale);
             this.target.setSize(width, height);
             this.outlinesTarget.setSize(width, height);
             this.ssaoDepthTexture.define(sw, sh);
@@ -349,8 +361,8 @@ export class PostprocessingPass {
             ValueCell.update(this.renderable.values.uTexSize, Vec2.set(this.renderable.values.uTexSize.ref.value, width, height));
             ValueCell.update(this.outlinesRenderable.values.uTexSize, Vec2.set(this.outlinesRenderable.values.uTexSize.ref.value, width, height));
             ValueCell.update(this.ssaoRenderable.values.uTexSize, Vec2.set(this.ssaoRenderable.values.uTexSize.ref.value, sw, sh));
-            ValueCell.update(this.ssaoBlurFirstPassRenderable.values.uTexSize, Vec2.set(this.ssaoRenderable.values.uTexSize.ref.value, sw, sh));
-            ValueCell.update(this.ssaoBlurSecondPassRenderable.values.uTexSize, Vec2.set(this.ssaoRenderable.values.uTexSize.ref.value, sw, sh));
+            ValueCell.update(this.ssaoBlurFirstPassRenderable.values.uTexSize, Vec2.set(this.ssaoBlurFirstPassRenderable.values.uTexSize.ref.value, sw, sh));
+            ValueCell.update(this.ssaoBlurSecondPassRenderable.values.uTexSize, Vec2.set(this.ssaoBlurSecondPassRenderable.values.uTexSize.ref.value, sw, sh));
         }
     }
 
@@ -367,8 +379,22 @@ export class PostprocessingPass {
         Mat4.invert(invProjection, camera.projection);
 
         if (props.occlusion.name === 'on') {
-            ValueCell.updateIfChanged(this.ssaoRenderable.values.uProjection, camera.projection);
-            ValueCell.updateIfChanged(this.ssaoRenderable.values.uInvProjection, invProjection);
+            ValueCell.update(this.ssaoRenderable.values.uProjection, camera.projection);
+            ValueCell.update(this.ssaoRenderable.values.uInvProjection, invProjection);
+
+            const [w, h] = this.renderable.values.uTexSize.ref.value;
+            const b = this.ssaoRenderable.values.uBounds;
+            const v = camera.viewport;
+            const s = this.ssaoScale;
+            Vec4.set(b.ref.value,
+                Math.floor(v.x * s) / (w * s),
+                Math.floor(v.y * s) / (h * s),
+                Math.ceil((v.x + v.width) * s) / (w * s),
+                Math.ceil((v.y + v.height) * s) / (h * s)
+            );
+            ValueCell.update(b, b.ref.value);
+            ValueCell.update(this.ssaoBlurFirstPassRenderable.values.uBounds, b.ref.value);
+            ValueCell.update(this.ssaoBlurSecondPassRenderable.values.uBounds, b.ref.value);
 
             ValueCell.updateIfChanged(this.ssaoBlurFirstPassRenderable.values.uNear, camera.near);
             ValueCell.updateIfChanged(this.ssaoBlurSecondPassRenderable.values.uNear, camera.near);
@@ -376,7 +402,9 @@ export class PostprocessingPass {
             ValueCell.updateIfChanged(this.ssaoBlurFirstPassRenderable.values.uFar, camera.far);
             ValueCell.updateIfChanged(this.ssaoBlurSecondPassRenderable.values.uFar, camera.far);
 
-            if (this.ssaoBlurFirstPassRenderable.values.dOrthographic.ref.value !== orthographic) { needsUpdateSsaoBlur = true; }
+            if (this.ssaoBlurFirstPassRenderable.values.dOrthographic.ref.value !== orthographic) {
+                needsUpdateSsaoBlur = true;
+            }
             ValueCell.updateIfChanged(this.ssaoBlurFirstPassRenderable.values.dOrthographic, orthographic);
             ValueCell.updateIfChanged(this.ssaoBlurSecondPassRenderable.values.dOrthographic, orthographic);
 
@@ -384,7 +412,7 @@ export class PostprocessingPass {
                 needsUpdateSsao = true;
 
                 this.nSamples = props.occlusion.params.samples;
-                ValueCell.updateIfChanged(this.ssaoRenderable.values.uSamples, getSamples(this.randomHemisphereVector, this.nSamples));
+                ValueCell.update(this.ssaoRenderable.values.uSamples, getSamples(this.randomHemisphereVector, this.nSamples));
                 ValueCell.updateIfChanged(this.ssaoRenderable.values.dNSamples, this.nSamples);
             }
             ValueCell.updateIfChanged(this.ssaoRenderable.values.uRadius, Math.pow(2, props.occlusion.params.radius));
@@ -394,10 +422,10 @@ export class PostprocessingPass {
                 needsUpdateSsaoBlur = true;
 
                 this.blurKernelSize = props.occlusion.params.blurKernelSize;
-                let kernel = getBlurKernel(this.blurKernelSize);
+                const kernel = getBlurKernel(this.blurKernelSize);
 
-                ValueCell.updateIfChanged(this.ssaoBlurFirstPassRenderable.values.uKernel, kernel);
-                ValueCell.updateIfChanged(this.ssaoBlurSecondPassRenderable.values.uKernel, kernel);
+                ValueCell.update(this.ssaoBlurFirstPassRenderable.values.uKernel, kernel);
+                ValueCell.update(this.ssaoBlurSecondPassRenderable.values.uKernel, kernel);
                 ValueCell.updateIfChanged(this.ssaoBlurFirstPassRenderable.values.dOcclusionKernelSize, this.blurKernelSize);
                 ValueCell.updateIfChanged(this.ssaoBlurSecondPassRenderable.values.dOcclusionKernelSize, this.blurKernelSize);
             }
@@ -467,10 +495,10 @@ export class PostprocessingPass {
 
         if (props.occlusion.name === 'on') {
             const { x, y, width, height } = camera.viewport;
-            const sx = Math.floor(x * this.scale);
-            const sy = Math.floor(y * this.scale);
-            const sw = Math.floor(width * this.scale);
-            const sh = Math.floor(height * this.scale);
+            const sx = Math.floor(x * this.ssaoScale);
+            const sy = Math.floor(y * this.ssaoScale);
+            const sw = Math.ceil(width * this.ssaoScale);
+            const sh = Math.ceil(height * this.ssaoScale);
             this.webgl.gl.viewport(sx, sy, sw, sh);
             this.webgl.gl.scissor(sx, sy, sw, sh);
 

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

@@ -55,7 +55,7 @@ export interface GeometryUtils<G extends Geometry, P extends PD.Params = Geometr
     createValuesSimple(geometry: G, props: Partial<PD.Values<P>>, colorValue: Color, sizeValue: number, transform?: TransformData): V
     updateValues(values: V, props: PD.Values<P>): void
     updateBoundingSphere(values: V, geometry: G): void
-    createRenderableState(props: Partial<PD.Values<P>>): RenderableState
+    createRenderableState(props: PD.Values<P>): RenderableState
     updateRenderableState(state: RenderableState, props: PD.Values<P>): void
     createPositionIterator(geometry: G, transform: TransformData): LocationIterator
 }

+ 8 - 0
src/mol-geo/geometry/mesh/mesh-builder.ts

@@ -144,6 +144,14 @@ export namespace MeshBuilder {
         }
     }
 
+    export function addMesh(state: State, t: Mat4, mesh: Mesh) {
+        addPrimitive(state, t, {
+            vertices: mesh.vertexBuffer.ref.value.subarray(0, mesh.vertexCount * 3),
+            normals: mesh.normalBuffer.ref.value.subarray(0, mesh.vertexCount * 3),
+            indices: mesh.indexBuffer.ref.value.subarray(0, mesh.triangleCount * 3),
+        });
+    }
+
     export function getMesh (state: State): Mesh {
         const { vertices, normals, indices, groups, mesh } = state;
         const vb = ChunkedArray.compact(vertices, true) as Float32Array;

+ 78 - 0
src/mol-geo/primitive/torus.ts

@@ -0,0 +1,78 @@
+/**
+ * Copyright (c) 2021 mol* contributors, licensed under MIT, See LICENSE file for more info.
+ *
+ * @author Alexander Rose <alexander.rose@weirdbyte.de>
+ */
+
+// adapted from three.js, MIT License Copyright 2010-2021 three.js authors
+
+import { Vec3 } from '../../mol-math/linear-algebra';
+import { Primitive } from './primitive';
+
+export const DefaultTorusProps = {
+    radius: 1,
+    tube: 0.4,
+    radialSegments: 8,
+    tubularSegments: 6,
+    arc: Math.PI * 2,
+};
+export type TorusProps = Partial<typeof DefaultTorusProps>
+
+export function Torus(props?: TorusProps): Primitive {
+    const { radius, tube, radialSegments, tubularSegments, arc } = { ...DefaultTorusProps, ...props };
+
+    // buffers
+    const indices: number[] = [];
+    const vertices: number[] = [];
+    const normals: number[] = [];
+
+    // helper variables
+    const center = Vec3();
+    const vertex = Vec3();
+    const normal = Vec3();
+
+    // generate vertices and normals
+    for (let j = 0; j <= radialSegments; ++j) {
+        for (let i = 0; i <= tubularSegments; ++i) {
+            const u = i / tubularSegments * arc;
+            const v = j / radialSegments * Math.PI * 2;
+
+            // vertex
+            Vec3.set(
+                vertex,
+                (radius + tube * Math.cos(v)) * Math.cos(u),
+                (radius + tube * Math.cos(v)) * Math.sin(u),
+                tube * Math.sin(v)
+            );
+            vertices.push(...vertex);
+
+            // normal
+            Vec3.set(center, radius * Math.cos(u), radius * Math.sin(u), 0 );
+            Vec3.sub(normal, vertex, center);
+            Vec3.normalize(normal, normal);
+            normals.push(...normal);
+        }
+    }
+
+    // generate indices
+    for (let j = 1; j <= radialSegments; ++j) {
+        for (let i = 1; i <= tubularSegments; ++i) {
+
+            // indices
+            const a = (tubularSegments + 1) * j + i - 1;
+            const b = (tubularSegments + 1) * (j - 1) + i - 1;
+            const c = (tubularSegments + 1) * (j - 1) + i;
+            const d = (tubularSegments + 1) * j + i;
+
+            // faces
+            indices.push(a, b, d);
+            indices.push(b, c, d);
+        }
+    }
+
+    return {
+        vertices: new Float32Array(vertices),
+        normals: new Float32Array(normals),
+        indices: new Uint32Array(indices)
+    };
+}

+ 3 - 2
src/mol-gl/shader/postprocessing.frag.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>
  * @author Áron Samuel Kovács <aron.kovacs@mail.muni.cz>
@@ -88,7 +88,8 @@ float getSsao(vec2 coords) {
     } else if (rawSsao > 0.001) {
         return rawSsao;
     }
-    return 0.0;
+    // treat values close to 0.0 as errors and return no occlusion
+    return 1.0;
 }
 
 void main(void) {

+ 17 - 3
src/mol-gl/shader/ssao-blur.frag.ts

@@ -1,7 +1,8 @@
 /**
- * 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 Áron Samuel Kovács <aron.kovacs@mail.muni.cz>
+ * @author Alexander Rose <alexander.rose@weirdbyte.de>
  */
 
 export const ssaoBlur_frag = `
@@ -11,6 +12,7 @@ precision highp sampler2D;
 
 uniform sampler2D tSsaoDepth;
 uniform vec2 uTexSize;
+uniform vec4 uBounds;
 
 uniform float uKernel[dOcclusionKernelSize];
 
@@ -36,16 +38,25 @@ bool isBackground(const in float depth) {
     return depth == 1.0;
 }
 
+bool outsideBounds(const in vec2 p) {
+    return p.x < uBounds.x || p.y < uBounds.y || p.x > uBounds.z || p.y > uBounds.w;
+}
+
 void main(void) {
     vec2 coords = gl_FragCoord.xy / uTexSize;
 
     vec2 packedDepth = texture2D(tSsaoDepth, coords).zw;
 
+    if (outsideBounds(coords)) {
+        gl_FragColor = vec4(packUnitIntervalToRG(1.0), packedDepth);
+        return;
+    }
+
     float selfDepth = unpackRGToUnitInterval(packedDepth);
     // if background and if second pass
     if (isBackground(selfDepth) && uBlurDirectionY != 0.0) {
-       gl_FragColor = vec4(packUnitIntervalToRG(1.0), packedDepth);
-       return;
+        gl_FragColor = vec4(packUnitIntervalToRG(1.0), packedDepth);
+        return;
     }
 
     float selfViewZ = getViewZ(selfDepth);
@@ -57,6 +68,9 @@ void main(void) {
     // only if kernelSize is odd
     for (int i = -dOcclusionKernelSize / 2; i <= dOcclusionKernelSize / 2; i++) {
         vec2 sampleCoords = coords + float(i) * offset;
+        if (outsideBounds(sampleCoords)) {
+            continue;
+        }
 
         vec4 sampleSsaoDepth = texture2D(tSsaoDepth, sampleCoords);
 

+ 8 - 4
src/mol-gl/shader/ssao.frag.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>
  * @author Áron Samuel Kovács <aron.kovacs@mail.muni.cz>
@@ -13,14 +13,14 @@ precision highp sampler2D;
 #include common
 
 uniform sampler2D tDepth;
+uniform vec2 uTexSize;
+uniform vec4 uBounds;
 
 uniform vec3 uSamples[dNSamples];
 
 uniform mat4 uProjection;
 uniform mat4 uInvProjection;
 
-uniform vec2 uTexSize;
-
 uniform float uRadius;
 uniform float uBias;
 
@@ -46,8 +46,12 @@ bool isBackground(const in float depth) {
     return depth == 1.0;
 }
 
+bool outsideBounds(const in vec2 p) {
+    return p.x < uBounds.x || p.y < uBounds.y || p.x > uBounds.z || p.y > uBounds.w;
+}
+
 float getDepth(const in vec2 coords) {
-    return unpackRGBAToDepth(texture2D(tDepth, coords));
+    return outsideBounds(coords) ? 1.0 : unpackRGBAToDepth(texture2D(tDepth, coords));
 }
 
 vec3 normalFromDepth(const in float depth, const in float depth1, const in float depth2, vec2 offset1, vec2 offset2) {

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

@@ -333,7 +333,7 @@ namespace Mat4 {
         return out;
     }
 
-    export function invert(out: Mat4, a: Mat4) {
+    export function tryInvert(out: Mat4, a: Mat4) {
         const a00 = a[0], a01 = a[1], a02 = a[2], a03 = a[3],
             a10 = a[4], a11 = a[5], a12 = a[6], a13 = a[7],
             a20 = a[8], a21 = a[9], a22 = a[10], a23 = a[11],
@@ -356,8 +356,7 @@ namespace Mat4 {
         let det = b00 * b11 - b01 * b10 + b02 * b09 + b03 * b08 - b04 * b07 + b05 * b06;
 
         if (!det) {
-            console.warn('non-invertible matrix.', a);
-            return out;
+            return false;
         }
         det = 1.0 / det;
 
@@ -378,6 +377,13 @@ namespace Mat4 {
         out[14] = (a31 * b01 - a30 * b03 - a32 * b00) * det;
         out[15] = (a20 * b03 - a21 * b01 + a22 * b00) * det;
 
+        return true;
+    }
+
+    export function invert(out: Mat4, a: Mat4) {
+        if (!tryInvert(out, a)) {
+            console.warn('non-invertible matrix.', a);
+        }
         return out;
     }
 

+ 8 - 1
src/mol-plugin-state/manager/structure/selection.ts

@@ -10,7 +10,7 @@ import { BoundaryHelper } from '../../../mol-math/geometry/boundary-helper';
 import { Vec3 } from '../../../mol-math/linear-algebra';
 import { PrincipalAxes } from '../../../mol-math/linear-algebra/matrix/principal-axes';
 import { EmptyLoci, Loci } from '../../../mol-model/loci';
-import { Structure, StructureElement, StructureSelection } from '../../../mol-model/structure';
+import { QueryContext, Structure, StructureElement, StructureQuery, StructureSelection } from '../../../mol-model/structure';
 import { PluginContext } from '../../../mol-plugin/context';
 import { StateObjectRef } from '../../../mol-state';
 import { Task } from '../../../mol-task';
@@ -457,6 +457,13 @@ export class StructureSelectionManager extends StatefulPluginComponent<Structure
         this.triggerInteraction(modifier, loci, applyGranularity);
     }
 
+    fromCompiledQuery(modifier: StructureSelectionModifier, query: StructureQuery, applyGranularity = true) {
+        for (const s of this.applicableStructures) {
+            const loci = query(new QueryContext(s));
+            this.triggerInteraction(modifier, StructureSelection.toLociWithSourceUnits(loci), applyGranularity);
+        }
+    }
+
     fromSelectionQuery(modifier: StructureSelectionModifier, query: StructureSelectionQuery, applyGranularity = true) {
         this.plugin.runTask(Task.create('Structure Selection', async runtime => {
             for (const s of this.applicableStructures) {

+ 26 - 17
src/mol-plugin-ui/structure/measurements.tsx

@@ -10,14 +10,17 @@ import { Loci } from '../../mol-model/loci';
 import { StructureElement } from '../../mol-model/structure';
 import { StructureMeasurementCell, StructureMeasurementOptions, StructureMeasurementParams } from '../../mol-plugin-state/manager/structure/measurement';
 import { StructureSelectionHistoryEntry } from '../../mol-plugin-state/manager/structure/selection';
-import { PluginStateObject } from '../../mol-plugin-state/objects';
 import { PluginCommands } from '../../mol-plugin/commands';
+import { AngleData } from '../../mol-repr/shape/loci/angle';
+import { DihedralData } from '../../mol-repr/shape/loci/dihedral';
+import { DistanceData } from '../../mol-repr/shape/loci/distance';
+import { LabelData } from '../../mol-repr/shape/loci/label';
 import { angleLabel, dihedralLabel, distanceLabel, lociLabel } from '../../mol-theme/label';
 import { FiniteArray } from '../../mol-util/type-helpers';
 import { CollapsableControls, PurePluginUIComponent } from '../base';
 import { ActionMenu } from '../controls/action-menu';
 import { Button, ExpandGroup, IconButton, ToggleButton } from '../controls/common';
-import { Icon, PencilRulerSvg, SetSvg, ArrowUpwardSvg, ArrowDownwardSvg, DeleteOutlinedSvg, HelpOutlineSvg, AddSvg, TuneSvg, VisibilityOffOutlinedSvg, VisibilityOutlinedSvg, MoreHorizSvg } from '../controls/icons';
+import { AddSvg, ArrowDownwardSvg, ArrowUpwardSvg, DeleteOutlinedSvg, HelpOutlineSvg, Icon, MoreHorizSvg, PencilRulerSvg, SetSvg, TuneSvg, VisibilityOffOutlinedSvg, VisibilityOutlinedSvg } from '../controls/icons';
 import { ParameterControls } from '../controls/parameters';
 import { UpdateTransformControl } from '../state/update-transform';
 import { ToggleSelectionModeButton } from './selection';
@@ -216,7 +219,7 @@ class MeasurementEntry extends PurePluginUIComponent<{ cell: StructureMeasuremen
     }
 
     get selections() {
-        return this.props.cell.obj?.data.sourceData as ReadonlyArray<PluginStateObject.Molecule.Structure.SelectionEntry> | undefined;
+        return this.props.cell.obj?.data.sourceData as Partial<DistanceData & AngleData & DihedralData & LabelData> | undefined;
     }
 
     delete = () => {
@@ -234,8 +237,8 @@ class MeasurementEntry extends PurePluginUIComponent<{ cell: StructureMeasuremen
         if (!selections) return;
 
         this.plugin.managers.interactivity.lociHighlights.clearHighlights();
-        for (const d of selections) {
-            this.plugin.managers.interactivity.lociHighlights.highlight({ loci: d.loci }, false);
+        for (const loci of this.lociArray) {
+            this.plugin.managers.interactivity.lociHighlights.highlight({ loci }, false);
         }
         this.plugin.managers.interactivity.lociHighlights.highlight({ loci: this.props.cell.obj?.data.repr.getLoci()! }, false);
     }
@@ -250,21 +253,31 @@ class MeasurementEntry extends PurePluginUIComponent<{ cell: StructureMeasuremen
         const selections = this.selections;
         if (!selections) return;
 
-        const sphere = Loci.getBundleBoundingSphere(toLociBundle(selections));
+        const sphere = Loci.getBundleBoundingSphere({ loci: this.lociArray });
         if (sphere) {
             this.plugin.managers.camera.focusSphere(sphere);
         }
     }
 
+    private get lociArray(): FiniteArray<Loci> {
+        const selections = this.selections;
+        if (!selections) return [];
+        if (selections.infos) return [selections.infos[0].loci];
+        if (selections.pairs) return selections.pairs[0].loci;
+        if (selections.triples) return selections.triples[0].loci;
+        if (selections.quads) return selections.quads[0].loci;
+        return [];
+    }
+
     get label() {
         const selections = this.selections;
-        switch (selections?.length) {
-            case 1: return lociLabel(selections[0].loci, { condensed: true });
-            case 2: return distanceLabel(toLociBundle(selections), { condensed: true, unitLabel: this.plugin.managers.structure.measurement.state.options.distanceUnitLabel });
-            case 3: return angleLabel(toLociBundle(selections), { condensed: true });
-            case 4: return dihedralLabel(toLociBundle(selections), { condensed: true });
-            default: return '';
-        }
+
+        if (!selections) return '<empty>';
+        if (selections.infos) return lociLabel(selections.infos[0].loci, { condensed: true });
+        if (selections.pairs) return distanceLabel(selections.pairs[0], { condensed: true, unitLabel: this.plugin.managers.structure.measurement.state.options.distanceUnitLabel });
+        if (selections.triples) return angleLabel(selections.triples[0], { condensed: true });
+        if (selections.quads) return dihedralLabel(selections.quads[0], { condensed: true });
+        return '<empty>';
     }
 
     get actions(): ActionMenu.Items {
@@ -302,8 +315,4 @@ class MeasurementEntry extends PurePluginUIComponent<{ cell: StructureMeasuremen
             </>}
         </>;
     }
-}
-
-function toLociBundle(data: FiniteArray<{ loci: Loci }, any>): { loci: FiniteArray<Loci, any> } {
-    return { loci: (data.map(d => d.loci) as unknown as FiniteArray<Loci, any>) };
 }

+ 113 - 35
src/mol-plugin-ui/structure/selection.tsx

@@ -6,22 +6,24 @@
  */
 
 import * as React from 'react';
-import { StructureSelectionQueries, StructureSelectionQuery, getNonStandardResidueQueries, getElementQueries, getPolymerAndBranchedEntityQueries } from '../../mol-plugin-state/helpers/structure-selection-query';
+import { Structure } from '../../mol-model/structure/structure/structure';
+import { getElementQueries, getNonStandardResidueQueries, getPolymerAndBranchedEntityQueries, StructureSelectionQueries, StructureSelectionQuery } from '../../mol-plugin-state/helpers/structure-selection-query';
 import { InteractivityManager } from '../../mol-plugin-state/manager/interactivity';
 import { StructureComponentManager } from '../../mol-plugin-state/manager/structure/component';
-import { StructureRef, StructureComponentRef } from '../../mol-plugin-state/manager/structure/hierarchy-state';
+import { StructureComponentRef, StructureRef } from '../../mol-plugin-state/manager/structure/hierarchy-state';
 import { StructureSelectionModifier } from '../../mol-plugin-state/manager/structure/selection';
+import { PluginContext } from '../../mol-plugin/context';
+import { compileResidueListSelection } from '../../mol-script/util/residue-list';
 import { memoizeLatest } from '../../mol-util/memoize';
 import { ParamDefinition } from '../../mol-util/param-definition';
-import { stripTags } from '../../mol-util/string';
+import { capitalize, stripTags } from '../../mol-util/string';
 import { PluginUIComponent, PurePluginUIComponent } from '../base';
 import { ActionMenu } from '../controls/action-menu';
 import { Button, ControlGroup, IconButton, ToggleButton } from '../controls/common';
+import { BrushSvg, CancelOutlinedSvg, CloseSvg, CubeOutlineSvg, HelpOutlineSvg, Icon, IntersectSvg, RemoveSvg, RestoreSvg, SelectionModeSvg, SetSvg, SubtractSvg, UnionSvg } from '../controls/icons';
 import { ParameterControls, ParamOnChange, PureSelectControl } from '../controls/parameters';
-import { UnionSvg, SubtractSvg, IntersectSvg, SetSvg, CubeOutlineSvg, Icon, SelectionModeSvg, RemoveSvg, RestoreSvg, HelpOutlineSvg, CancelOutlinedSvg, BrushSvg, CloseSvg } from '../controls/icons';
+import { HelpGroup, HelpText, ViewportHelpContent } from '../viewport/help';
 import { AddComponentControls } from './components';
-import { Structure } from '../../mol-model/structure/structure/structure';
-import { ViewportHelpContent, HelpGroup, HelpText } from '../viewport/help';
 
 
 export class ToggleSelectionModeButton extends PurePluginUIComponent<{ inline?: boolean }> {
@@ -47,12 +49,15 @@ const StructureSelectionParams = {
     granularity: InteractivityManager.Params.granularity,
 };
 
+type SelectionHelperType = 'residue-list'
+
 interface StructureSelectionActionsControlsState {
     isEmpty: boolean,
     isBusy: boolean,
     canUndo: boolean,
 
-    action?: StructureSelectionModifier | 'theme' | 'add-component' | 'help'
+    action?: StructureSelectionModifier | 'theme' | 'add-component' | 'help',
+    helper?: SelectionHelperType,
 }
 
 const ActionHeader = new Map<StructureSelectionModifier, string>([
@@ -65,6 +70,7 @@ const ActionHeader = new Map<StructureSelectionModifier, string>([
 export class StructureSelectionActionsControls extends PluginUIComponent<{}, StructureSelectionActionsControlsState> {
     state = {
         action: void 0 as StructureSelectionActionsControlsState['action'],
+        helper: void 0 as StructureSelectionActionsControlsState['helper'],
 
         isEmpty: true,
         isBusy: false,
@@ -118,7 +124,16 @@ export class StructureSelectionActionsControls extends PluginUIComponent<{}, Str
         }
     }
 
-    get structures () {
+    selectHelper: ActionMenu.OnSelect = (item, e) => {
+        console.log(item);
+        if (!item || !this.state.action) {
+            this.setState({ action: void 0, helper: void 0 });
+            return;
+        }
+        this.setState({ helper: (item.value as { kind: SelectionHelperType }).kind });
+    }
+
+    get structures() {
         const structures: Structure[] = [];
         for (const s of this.plugin.managers.structure.hierarchy.selection.structures) {
             const structure = s.cell.obj?.data;
@@ -129,7 +144,7 @@ export class StructureSelectionActionsControls extends PluginUIComponent<{}, Str
 
     private queriesItems: ActionMenu.Items[] = []
     private queriesVersion = -1
-    get queries () {
+    get queries() {
         const { registry } = this.plugin.query.structure;
         if (registry.version !== this.queriesVersion) {
             const structures = this.structures;
@@ -150,8 +165,25 @@ export class StructureSelectionActionsControls extends PluginUIComponent<{}, Str
         return this.queriesItems;
     }
 
+    private helpersItems?: ActionMenu.Items[] = void 0;
+    get helpers() {
+        if (this.helpersItems) return this.helpersItems;
+        // TODO: this is an initial implementation of the helper UI
+        //       the plan is to add support to input queries in different languages
+        //       after this has been implemented in mol-script
+        const helpers = [
+            { kind: 'residue-list' as SelectionHelperType, category: 'Helpers', label: 'Residue List', description: 'Create a selection from a list of residue ranges.' }
+        ];
+        this.helpersItems = ActionMenu.createItems(helpers, {
+            label: q => q.label,
+            category: q => q.category,
+            description: q => q.description
+        });
+        return this.helpersItems;
+    }
+
     private showAction(q: StructureSelectionActionsControlsState['action']) {
-        return () => this.setState({ action: this.state.action === q ? void 0 : q });
+        return () => this.setState({ action: this.state.action === q ? void 0 : q, helper: void 0 });
     }
 
     toggleAdd = this.showAction('add')
@@ -187,6 +219,45 @@ export class StructureSelectionActionsControls extends PluginUIComponent<{}, Str
             ? `Undo ${this.plugin.state.data.latestUndoLabel}`
             : 'Some mistakes of the past can be undone.';
 
+        let children: React.ReactNode | undefined = void 0;
+
+        if (this.state.action && !this.state.helper) {
+            children = <>
+                {(this.state.action && this.state.action !== 'theme' && this.state.action !== 'add-component' && this.state.action !== 'help') && <div className='msp-selection-viewport-controls-actions'>
+                    <ActionMenu header={ActionHeader.get(this.state.action as StructureSelectionModifier)} title='Click to close.' items={this.queries} onSelect={this.selectQuery} noOffset />
+                    <ActionMenu items={this.helpers} onSelect={this.selectHelper} noOffset />
+                </div>}
+                {this.state.action === 'theme' && <div className='msp-selection-viewport-controls-actions'>
+                    <ControlGroup header='Theme' title='Click to close.' initialExpanded={true} hideExpander={true} hideOffset={true} onHeaderClick={this.toggleTheme} topRightIcon={CloseSvg}>
+                        <ApplyThemeControls onApply={this.toggleTheme} />
+                    </ControlGroup>
+                </div>}
+                {this.state.action === 'add-component' && <div className='msp-selection-viewport-controls-actions'>
+                    <ControlGroup header='Add Component' title='Click to close.' initialExpanded={true} hideExpander={true} hideOffset={true} onHeaderClick={this.toggleAddComponent} topRightIcon={CloseSvg}>
+                        <AddComponentControls onApply={this.toggleAddComponent} forSelection />
+                    </ControlGroup>
+                </div>}
+                {this.state.action === 'help' && <div className='msp-selection-viewport-controls-actions'>
+                    <ControlGroup header='Help' title='Click to close.' initialExpanded={true} hideExpander={true} hideOffset={true} onHeaderClick={this.toggleHelp} topRightIcon={CloseSvg} maxHeight='300px'>
+                        <HelpGroup header='Selection Operations'>
+                            <HelpText>Use <Icon svg={UnionSvg} inline /> <Icon svg={SubtractSvg} inline /> <Icon svg={IntersectSvg} inline /> <Icon svg={SetSvg} inline /> to modify the selection.</HelpText>
+                        </HelpGroup>
+                        <HelpGroup header='Representation Operations'>
+                            <HelpText>Use <Icon svg={BrushSvg} inline /> <Icon svg={CubeOutlineSvg} inline /> <Icon svg={RemoveSvg} inline /> <Icon svg={RestoreSvg} inline /> to color, create components, remove from components, or undo actions.</HelpText>
+                        </HelpGroup>
+                        <ViewportHelpContent selectOnly={true} />
+                    </ControlGroup>
+                </div>}
+            </>;
+        } else if (ActionHeader.has(this.state.action as any) && this.state.helper === 'residue-list') {
+            const close = () => this.setState({ action: void 0, helper: void 0 });
+            children = <div className='msp-selection-viewport-controls-actions'>
+                <ControlGroup header='Residue List' title='Click to close.' initialExpanded={true} hideExpander={true} hideOffset={true} onHeaderClick={close} topRightIcon={CloseSvg}>
+                    <ResidueListSelectionHelper modifier={this.state.action as any} plugin={this.plugin} close={close} />
+                </ControlGroup>
+            </div>;
+        }
+
         return <>
             <div className='msp-flex-row' style={{ background: 'none' }}>
                 <PureSelectControl title={`Picking Level for selecting and highlighting`} param={StructureSelectionParams.granularity} name='granularity' value={granularity} onChange={this.setGranuality} isDisabled={this.isDisabled} />
@@ -195,7 +266,7 @@ export class StructureSelectionActionsControls extends PluginUIComponent<{}, Str
                 <ToggleButton icon={IntersectSvg} title={`${ActionHeader.get('intersect')}. Hold shift key to keep menu open.`} toggle={this.toggleIntersect} isSelected={this.state.action === 'intersect'} disabled={this.isDisabled} />
                 <ToggleButton icon={SetSvg} title={`${ActionHeader.get('set')}. Hold shift key to keep menu open.`} toggle={this.toggleSet} isSelected={this.state.action === 'set'} disabled={this.isDisabled} />
 
-                <ToggleButton icon={BrushSvg} title='Apply Theme to Selection' toggle={this.toggleTheme} isSelected={this.state.action === 'theme'} disabled={this.isDisabled} style={{ marginLeft: '10px' }}  />
+                <ToggleButton icon={BrushSvg} title='Apply Theme to Selection' toggle={this.toggleTheme} isSelected={this.state.action === 'theme'} disabled={this.isDisabled} style={{ marginLeft: '10px' }} />
                 <ToggleButton icon={CubeOutlineSvg} title='Create Component of Selection with Representation' toggle={this.toggleAddComponent} isSelected={this.state.action === 'add-component'} disabled={this.isDisabled} />
                 <IconButton svg={RemoveSvg} title='Remove/subtract Selection from all Components' onClick={this.subtract} disabled={this.isDisabled} />
                 <IconButton svg={RestoreSvg} onClick={this.undo} disabled={!this.state.canUndo || this.isDisabled} title={undoTitle} />
@@ -203,30 +274,7 @@ export class StructureSelectionActionsControls extends PluginUIComponent<{}, Str
                 <ToggleButton icon={HelpOutlineSvg} title='Show/hide help' toggle={this.toggleHelp} style={{ marginLeft: '10px' }} isSelected={this.state.action === 'help'} />
                 <IconButton svg={CancelOutlinedSvg} title='Turn selection mode off' onClick={this.turnOff} />
             </div>
-            {(this.state.action && this.state.action !== 'theme' && this.state.action !== 'add-component' && this.state.action !== 'help') && <div className='msp-selection-viewport-controls-actions'>
-                <ActionMenu header={ActionHeader.get(this.state.action as StructureSelectionModifier)} title='Click to close.' items={this.queries} onSelect={this.selectQuery} noOffset />
-            </div>}
-            {this.state.action === 'theme' && <div className='msp-selection-viewport-controls-actions'>
-                <ControlGroup header='Theme' title='Click to close.' initialExpanded={true} hideExpander={true} hideOffset={true} onHeaderClick={this.toggleTheme} topRightIcon={CloseSvg}>
-                    <ApplyThemeControls onApply={this.toggleTheme} />
-                </ControlGroup>
-            </div>}
-            {this.state.action === 'add-component' && <div className='msp-selection-viewport-controls-actions'>
-                <ControlGroup header='Add Component' title='Click to close.' initialExpanded={true} hideExpander={true} hideOffset={true} onHeaderClick={this.toggleAddComponent} topRightIcon={CloseSvg}>
-                    <AddComponentControls onApply={this.toggleAddComponent} forSelection />
-                </ControlGroup>
-            </div>}
-            {this.state.action === 'help' && <div className='msp-selection-viewport-controls-actions'>
-                <ControlGroup header='Help' title='Click to close.' initialExpanded={true} hideExpander={true} hideOffset={true} onHeaderClick={this.toggleHelp} topRightIcon={CloseSvg} maxHeight='300px'>
-                    <HelpGroup header='Selection Operations'>
-                        <HelpText>Use <Icon svg={UnionSvg} inline /> <Icon svg={SubtractSvg} inline /> <Icon svg={IntersectSvg} inline /> <Icon svg={SetSvg} inline /> to modify the selection.</HelpText>
-                    </HelpGroup>
-                    <HelpGroup header='Representation Operations'>
-                        <HelpText>Use <Icon svg={BrushSvg} inline /> <Icon svg={CubeOutlineSvg} inline /> <Icon svg={RemoveSvg} inline /> <Icon svg={RestoreSvg} inline /> to color, create components, remove from components, or undo actions.</HelpText>
-                    </HelpGroup>
-                    <ViewportHelpContent selectOnly={true} />
-                </ControlGroup>
-            </div>}
+            {children}
         </>;
     }
 }
@@ -333,4 +381,34 @@ class ApplyThemeControls extends PurePluginUIComponent<ApplyThemeControlsProps,
             </Button>
         </>;
     }
+}
+
+const ResidueListIdTypeParams = {
+    idType: ParamDefinition.Select<'auth' | 'label'>('auth', ParamDefinition.arrayToOptions(['auth', 'label'])),
+    residues: ParamDefinition.Text('', { description: 'A comma separated list of residue ranges in given chain, e.g. A 10-15, B 25, C 30:i' })
+};
+
+const DefaultResidueListIdTypeParams = ParamDefinition.getDefaultValues(ResidueListIdTypeParams);
+
+function ResidueListSelectionHelper({ modifier, plugin, close }: { modifier: StructureSelectionModifier, plugin: PluginContext, close: () => void }) {
+    const [state, setState] = React.useState(DefaultResidueListIdTypeParams);
+
+    const apply = () => {
+        if (state.residues.length === 0) return;
+
+        try {
+            close();
+            const query = compileResidueListSelection(state.residues, state.idType);
+            plugin.managers.structure.selection.fromCompiledQuery(modifier, query, false);
+        } catch (e) {
+            plugin.log.error(`Failed to create selection: ${e}`);
+        }
+    };
+
+    return <>
+        <ParameterControls params={ResidueListIdTypeParams} values={state} onChangeValues={setState} onEnter={apply} />
+        <Button className='msp-btn-commit msp-btn-commit-on' disabled={state.residues.length === 0} onClick={apply} style={{ marginTop: '1px' }}>
+            {capitalize(modifier)} Selection
+        </Button>
+    </>;
 }

+ 11 - 8
src/mol-plugin-ui/viewport/screenshot.tsx

@@ -34,12 +34,16 @@ export class DownloadScreenshotControls extends PluginUIComponent<{ close: () =>
     }
 
     private copy = async () => {
-        await this.plugin.helpers.viewportScreenshot?.copyToClipboard();
-        PluginCommands.Toast.Show(this.plugin, {
-            message: 'Copied to clipboard.',
-            title: 'Screenshot',
-            timeoutMs: 1500
-        });
+        try {
+            await this.plugin.helpers.viewportScreenshot?.copyToClipboard();
+            PluginCommands.Toast.Show(this.plugin, {
+                message: 'Copied to clipboard.',
+                title: 'Screenshot',
+                timeoutMs: 1500
+            });
+        } catch {
+            return this.copyImg();
+        }
     }
 
     private copyImg = async () => {
@@ -71,8 +75,7 @@ export class DownloadScreenshotControls extends PluginUIComponent<{ close: () =>
                 <CropControls plugin={this.plugin} />
             </div>}
             <div className='msp-flex-row'>
-                {hasClipboardApi && <Button icon={CopySvg} onClick={this.copy} disabled={this.state.isDisabled}>Copy</Button>}
-                {!hasClipboardApi && !this.state.imageData && <Button icon={CopySvg} onClick={this.copyImg} disabled={this.state.isDisabled}>Copy</Button>}
+                {!this.state.imageData && <Button icon={CopySvg} onClick={hasClipboardApi ? this.copy : this.copyImg} disabled={this.state.isDisabled}>Copy</Button>}
                 {this.state.imageData && <Button onClick={() => this.setState({ imageData: void 0 })} disabled={this.state.isDisabled}>Clear</Button>}
                 <Button icon={GetAppSvg} onClick={this.download} disabled={this.state.isDisabled}>Download</Button>
             </div>

+ 11 - 7
src/mol-plugin/context.ts

@@ -184,17 +184,21 @@ export class PluginContext {
      */
     readonly customState: unknown = Object.create(null);
 
-    initViewer(canvas: HTMLCanvasElement, container: HTMLDivElement) {
+    initViewer(canvas: HTMLCanvasElement, container: HTMLDivElement, canvas3dContext?: Canvas3DContext) {
         try {
             this.layout.setRoot(container);
             if (this.spec.layout && this.spec.layout.initial) this.layout.setProps(this.spec.layout.initial);
 
-            const antialias = !(this.config.get(PluginConfig.General.DisableAntialiasing) ?? false);
-            const preserveDrawingBuffer = !(this.config.get(PluginConfig.General.DisablePreserveDrawingBuffer) ?? false);
-            const pixelScale = this.config.get(PluginConfig.General.PixelScale) || 1;
-            const pickScale = this.config.get(PluginConfig.General.PickScale) || 0.25;
-            const enableWboit = this.config.get(PluginConfig.General.EnableWboit) || false;
-            (this.canvas3dContext as Canvas3DContext) = Canvas3DContext.fromCanvas(canvas, { antialias, preserveDrawingBuffer, pixelScale, pickScale, enableWboit });
+            if (canvas3dContext) {
+                (this.canvas3dContext as Canvas3DContext) = canvas3dContext;
+            } else {
+                const antialias = !(this.config.get(PluginConfig.General.DisableAntialiasing) ?? false);
+                const preserveDrawingBuffer = !(this.config.get(PluginConfig.General.DisablePreserveDrawingBuffer) ?? false);
+                const pixelScale = this.config.get(PluginConfig.General.PixelScale) || 1;
+                const pickScale = this.config.get(PluginConfig.General.PickScale) || 0.25;
+                const enableWboit = this.config.get(PluginConfig.General.EnableWboit) || false;
+                (this.canvas3dContext as Canvas3DContext) = Canvas3DContext.fromCanvas(canvas, { antialias, preserveDrawingBuffer, pixelScale, pickScale, enableWboit });
+            }
             (this.canvas3d as Canvas3D) = Canvas3D.create(this.canvas3dContext!);
             this.canvas3dInit.next(true);
             let props = this.spec.canvas3d;

+ 1 - 1
src/mol-plugin/util/viewport-screenshot.ts

@@ -324,7 +324,7 @@ class ViewportScreenshotHelper extends PluginComponent {
             await ctx.update('Converting image...');
             const blob = await canvasToBlob(this.canvas, 'png');
             const item = new ClipboardItem({ 'image/png': blob });
-            cb.write([item]);
+            await cb.write([item]);
             this.plugin.log.message('Image copied to clipboard.');
         });
     }

+ 72 - 0
src/mol-script/util/residue-list.ts

@@ -0,0 +1,72 @@
+/**
+ * Copyright (c) 2021 mol* contributors, licensed under MIT, See LICENSE file for more info.
+ *
+ * @author David Sehnal <david.sehnal@gmail.com>
+ */
+
+import { StructureQuery } from '../../mol-model/structure/query';
+import { Expression } from '../language/expression';
+import { MolScriptBuilder as MS } from '../language/builder';
+import { compile } from '../runtime/query/base';
+
+// TODO: make this into a separate "language"?
+
+type ResidueListSelectionEntry =
+    | { kind: 'single', asym_id: string; seq_id: number; ins_code?: string }
+    | { kind: 'range', asym_id: string; seq_id_beg: number; seq_id_end: number; }
+
+function entriesToQuery(xs: ResidueListSelectionEntry[], kind: 'auth' | 'label') {
+    const groups: Expression[] = [];
+
+    const asym_id_key = kind === 'auth' ? 'auth_asym_id' as const : 'label_asym_id' as const;
+    const seq_id_key = kind === 'auth' ? 'auth_seq_id' as const : 'label_seq_id' as const;
+
+    for (const x of xs) {
+        if (x.kind === 'range') {
+            groups.push(MS.struct.generator.atomGroups({
+                'chain-test': MS.core.rel.eq([MS.ammp(asym_id_key), x.asym_id]),
+                'residue-test': MS.core.rel.inRange([MS.ammp(seq_id_key), x.seq_id_beg, x.seq_id_end])
+            }));
+        } else {
+            const ins_code = (x.ins_code ?? '').trim();
+
+            groups.push(MS.struct.generator.atomGroups({
+                'chain-test': MS.core.rel.eq([MS.ammp(asym_id_key), x.asym_id]),
+                'residue-test': MS.core.logic.and([
+                    MS.core.rel.eq([MS.ammp(seq_id_key), x.seq_id]),
+                    MS.core.rel.eq([MS.ammp('pdbx_PDB_ins_code'), ins_code])
+                ])
+            }));
+        }
+    }
+
+    const query = MS.struct.combinator.merge(groups);
+
+    return compile(query) as StructureQuery;
+}
+
+function parseRange(c: string, s: string[], e: number): ResidueListSelectionEntry | undefined {
+    if (!c || s.length === 0 || Number.isNaN(+s[0])) return;
+    if (Number.isNaN(e)) {
+        return { kind: 'single', asym_id: c, seq_id: +s[0], ins_code: s[1] };
+    }
+    return { kind: 'range', asym_id: c, seq_id_beg: +s[0], seq_id_end: e };
+}
+
+function parseInsCode(e?: string) {
+    if (!e) return [];
+    return e.split(':');
+}
+
+function parseResidueListSelection(input: string): ResidueListSelectionEntry[] {
+    return input.split(',') // A 1-3, B 3 => [A 1-3, B 3]
+        .map(e => e.trim().split(/\s+|[-]/g).filter(e => !!e)) // [A 1-3, B 3] => [[A, 1, 3], [B, 3]]
+        .map(e => parseRange(e[0], parseInsCode(e[1]), +e[2]))
+        .filter(e => !!e) as ResidueListSelectionEntry[];
+}
+
+// parses a list of residue ranges, e.g. A 10-100, B 30, C 12:i
+export function compileResidueListSelection(input: string, idType: 'auth' | 'label') {
+    const entries = parseResidueListSelection(input);
+    return entriesToQuery(entries, idType);
+}

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

@@ -105,7 +105,12 @@ async function init() {
 
     const mcBoundingSphere = Sphere3D.fromBox3D(Sphere3D(), densityTextureData.bbox);
     const mcIsosurface = TextureMesh.create(gv.vertexCount, 1, gv.vertexTexture, gv.groupTexture, gv.normalTexture, mcBoundingSphere);
-    const mcIsoSurfaceProps = { doubleSided: true, flatShaded: true, alpha: 1.0 };
+    const mcIsoSurfaceProps = {
+        ...PD.getDefaultValues(TextureMesh.Params),
+        doubleSided: true,
+        flatShaded: true,
+        alpha: 1.0
+    };
     const mcIsoSurfaceValues = TextureMesh.Utils.createValuesSimple(mcIsosurface, mcIsoSurfaceProps, Color(0x112299), 1);
     // console.log('mcIsoSurfaceValues', mcIsoSurfaceValues)
     const mcIsoSurfaceState = TextureMesh.Utils.createRenderableState(mcIsoSurfaceProps);
@@ -133,7 +138,12 @@ async function init() {
     console.timeEnd('cpu mc');
     console.log('surface', surface);
     Mesh.transform(surface, densityData.transform);
-    const meshProps = { doubleSided: true, flatShaded: false, alpha: 1.0 };
+    const meshProps = {
+        ...PD.getDefaultValues(Mesh.Params),
+        doubleSided: true,
+        flatShaded: false,
+        alpha: 1.0
+    };
     const meshValues = Mesh.Utils.createValuesSimple(surface, meshProps, Color(0x995511), 1);
     const meshState = Mesh.Utils.createRenderableState(meshProps);
     const meshRenderObject = createRenderObject('mesh', meshValues, meshState, -1);

+ 4 - 2
src/tests/browser/render-lines.ts

@@ -14,6 +14,7 @@ import { Lines } from '../../mol-geo/geometry/lines/lines';
 import { Color } from '../../mol-util/color';
 import { createRenderObject } from '../../mol-gl/render-object';
 import { Representation } from '../../mol-repr/representation';
+import { ParamDefinition } from '../../mol-util/param-definition';
 
 const parent = document.getElementById('app')!;
 parent.style.width = '100%';
@@ -33,8 +34,9 @@ function linesRepr() {
     linesBuilder.addCage(t, dodecahedronCage, 0);
     const lines = linesBuilder.getLines();
 
-    const values = Lines.Utils.createValuesSimple(lines, {}, Color(0xFF0000), 3);
-    const state = Lines.Utils.createRenderableState({});
+    const props = ParamDefinition.getDefaultValues(Lines.Utils.Params);
+    const values = Lines.Utils.createValuesSimple(lines, props, Color(0xFF0000), 3);
+    const state = Lines.Utils.createRenderableState(props);
     const renderObject = createRenderObject('lines', values, state, -1);
     const repr = Representation.fromRenderObject('cage-lines', renderObject);
     return repr;

+ 14 - 5
src/tests/browser/render-mesh.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>
  */
@@ -15,6 +15,8 @@ import { Mesh } from '../../mol-geo/geometry/mesh/mesh';
 import { Color } from '../../mol-util/color';
 import { createRenderObject } from '../../mol-gl/render-object';
 import { Representation } from '../../mol-repr/representation';
+import { Torus } from '../../mol-geo/primitive/torus';
+import { ParamDefinition } from '../../mol-util/param-definition';
 
 const parent = document.getElementById('app')!;
 parent.style.width = '100%';
@@ -31,17 +33,24 @@ function meshRepr() {
     const builderState = MeshBuilder.createState();
 
     const t = Mat4.identity();
-    MeshBuilder.addCage(builderState, t, HexagonalPrismCage(), 0.005, 2, 20);
+    Mat4.scaleUniformly(t, t, 10);
+    MeshBuilder.addCage(builderState, t, HexagonalPrismCage(), 0.05, 2, 20);
 
     const t2 = Mat4.identity();
-    Mat4.scaleUniformly(t2, t2, 0.1);
+    Mat4.scaleUniformly(t2, t2, 1);
     MeshBuilder.addPrimitive(builderState, t2, SpikedBall(3));
 
+    const t3 = Mat4.identity();
+    Mat4.scaleUniformly(t3, t3, 8);
+    MeshBuilder.addPrimitive(builderState, t3, Torus({ tubularSegments: 64, radialSegments: 32, tube: 0.1 }));
+
     const mesh = MeshBuilder.getMesh(builderState);
 
-    const values = Mesh.Utils.createValuesSimple(mesh, {}, Color(0xFF0000), 1);
-    const state = Mesh.Utils.createRenderableState({});
+    const props = ParamDefinition.getDefaultValues(Mesh.Utils.Params);
+    const values = Mesh.Utils.createValuesSimple(mesh, props, Color(0xFF4433), 1);
+    const state = Mesh.Utils.createRenderableState(props);
     const renderObject = createRenderObject('mesh', values, state, -1);
+    console.log('mesh', renderObject);
     const repr = Representation.fromRenderObject('mesh', renderObject);
     return repr;
 }

+ 1 - 0
src/tests/browser/render-shape.ts

@@ -111,6 +111,7 @@ const repr = ShapeRepresentation(getShape, Mesh.Utils);
 export async function init() {
     // Create shape from myData and add to canvas3d
     await repr.createOrUpdate({}, myData).run((p: Progress) => console.log(Progress.format(p)));
+    console.log('shape', repr);
     canvas3d.add(repr);
     canvas3d.requestCameraReset();
 

+ 3 - 1
src/tests/browser/render-spheres.ts

@@ -12,6 +12,7 @@ import { Spheres } from '../../mol-geo/geometry/spheres/spheres';
 import { Color } from '../../mol-util/color';
 import { createRenderObject } from '../../mol-gl/render-object';
 import { Representation } from '../../mol-repr/representation';
+import { ParamDefinition } from '../../mol-util/param-definition';
 
 const parent = document.getElementById('app')!;
 parent.style.width = '100%';
@@ -31,8 +32,9 @@ function spheresRepr() {
     spheresBuilder.add(-4, 1, 0, 0);
     const spheres = spheresBuilder.getSpheres();
 
+    const props = ParamDefinition.getDefaultValues(Spheres.Utils.Params);
     const values = Spheres.Utils.createValuesSimple(spheres, {}, Color(0xFF0000), 1);
-    const state = Spheres.Utils.createRenderableState({});
+    const state = Spheres.Utils.createRenderableState(props);
     const renderObject = createRenderObject('spheres', values, state, -1);
     console.log(renderObject);
     const repr = Representation.fromRenderObject('spheres', renderObject);

+ 4 - 3
src/tests/browser/render-text.ts

@@ -8,7 +8,7 @@ import './index.html';
 import { Canvas3D, Canvas3DContext } from '../../mol-canvas3d/canvas3d';
 import { TextBuilder } from '../../mol-geo/geometry/text/text-builder';
 import { Text } from '../../mol-geo/geometry/text/text';
-import { ParamDefinition as PD } from '../../mol-util/param-definition';
+import { ParamDefinition, ParamDefinition as PD } from '../../mol-util/param-definition';
 import { Color } from '../../mol-util/color';
 import { Representation } from '../../mol-repr/representation';
 import { SpheresBuilder } from '../../mol-geo/geometry/spheres/spheres-builder';
@@ -64,8 +64,9 @@ function spheresRepr() {
     spheresBuilder.add(-4, 1, 0, 0);
     const spheres = spheresBuilder.getSpheres();
 
-    const values = Spheres.Utils.createValuesSimple(spheres, {}, Color(0xFF0000), 0.2);
-    const state = Spheres.Utils.createRenderableState({});
+    const props = ParamDefinition.getDefaultValues(Spheres.Utils.Params);
+    const values = Spheres.Utils.createValuesSimple(spheres, props, Color(0xFF0000), 0.2);
+    const state = Spheres.Utils.createRenderableState(props);
     const renderObject = createRenderObject('spheres', values, state, -1);
     console.log('spheres', renderObject);
     const repr = Representation.fromRenderObject('spheres', renderObject);