Browse Source

Merge branch 'master' of https://github.com/molstar/molstar into traj-anim

Alexander Rose 3 years ago
parent
commit
1748efbc18

+ 7 - 0
CHANGELOG.md

@@ -9,6 +9,10 @@ Note that since we don't clearly distinguish between a public and private interf
 - Fix parsing contour-level from emdb v3 header files
 - Fix invalid CSS (#376)
 - Fix "texture not renderable" & "texture not bound" warnings (#319)
+- Fix visual for bonds between two aromatic rings
+- Fix visual for delocalized bonds (parsed from mmcif and mol2)
+- Fix ring computation algorithm
+- Add ``UnitResonance`` property with info about delocalized triplets
 - Resolve marking in main renderer loop to improve overall performance
 - Use ``throttleTime`` instead of ``debounceTime`` in sequence viewer for better responsiveness
 - Change line geometry default ``scaleFactor`` to 2 (3 is too big after fixing line rendering)
@@ -19,6 +23,9 @@ Note that since we don't clearly distinguish between a public and private interf
     - Don't show 'inter-bond' and 'element-cross' visuals in line representations of polymerAndLigand preset
 - Fix additional mononucleotides detected as polymer components
 - Fix and improve ``canRemap`` handling in ``IntraUnitBonds``
+- Reuse occlusion for secondary passes during multi-sampling
+- Check if marking passes are needed before doing them
+- Add ``scaleFactor`` parameter to adjust resolution of occlusion calculation
 
 ## [v3.2.0] - 2022-02-17
 

+ 8 - 0
docs/interesting-pdb-entries.md

@@ -34,6 +34,14 @@
     * ACE (many, e.g. 5AGU, 1E1X)
     * ACY in 7ABY
     * NH2 (many, e.g. 6Y13)
+* Ligands with many rings
+    * STU (e.g. 1U59) - many fused rings
+    * HT (e.g. 127D) - rings connected by a single bond
+    * J2C (e.g. 7EFJ) - rings connected by a single atom
+    * RBF (e.g. 7QF2) - three linearly fused rings
+    * TA1 (e.g. 1JFF) - many fused rings (incl. a 8-member rings)
+    * BPA (e.g. 1JDG) - many fused rings
+    * CLR (e.g. 3GKI) - four fused rings
 
 Assembly symmetries
 * 5M30 (Assembly 1, C3 local and pseudo)

+ 5 - 4
src/apps/docking-viewer/viewport.tsx

@@ -44,10 +44,11 @@ function occlusionStyle(plugin: PluginContext) {
         postprocessing: {
             ...plugin.canvas3d!.props.postprocessing,
             occlusion: { name: 'on', params: {
-                samples: 64,
-                radius: 8,
-                bias: 1.0,
-                blurKernelSize: 13
+                bias: 0.8,
+                blurKernelSize: 15,
+                radius: 5,
+                samples: 32,
+                scaleFactor: 1
             } },
             outline: { name: 'on', params: {
                 scale: 1.0,

+ 2 - 2
src/examples/lighting/index.ts

@@ -24,7 +24,7 @@ const Canvas3DPresets = {
     illustrative: {
         canvas3d: <Preset>{
             postprocessing: {
-                occlusion: { name: 'on', params: { samples: 32, radius: 6, bias: 1.4, blurKernelSize: 15 } },
+                occlusion: { name: 'on', params: { samples: 32, radius: 6, bias: 1.4, blurKernelSize: 15, scaleFactor: 1 } },
                 outline: { name: 'on', params: { scale: 1, threshold: 0.33, color: Color(0x000000) } }
             },
             renderer: {
@@ -36,7 +36,7 @@ const Canvas3DPresets = {
     occlusion: {
         canvas3d: <Preset>{
             postprocessing: {
-                occlusion: { name: 'on', params: { samples: 32, radius: 6, bias: 1.4, blurKernelSize: 15 } },
+                occlusion: { name: 'on', params: { samples: 32, radius: 6, bias: 1.4, blurKernelSize: 15, scaleFactor: 1 } },
                 outline: { name: 'off', params: {} }
             },
             renderer: {

+ 14 - 11
src/mol-canvas3d/passes/draw.ts

@@ -307,19 +307,22 @@ export class DrawPass {
         }
 
         if (markingEnabled) {
-            const markingDepthTest = props.marking.ghostEdgeStrength < 1;
-            if (markingDepthTest) {
-                this.marking.depthTarget.bind();
+            const markerAverage = scene.getMarkerAverage();
+            if (markerAverage > 0) {
+                const markingDepthTest = props.marking.ghostEdgeStrength < 1;
+                if (markingDepthTest && markerAverage !== 1) {
+                    this.marking.depthTarget.bind();
+                    renderer.clear(false, true);
+                    renderer.renderMarkingDepth(scene.primitives, camera, null);
+                }
+
+                this.marking.maskTarget.bind();
                 renderer.clear(false, true);
-                renderer.renderMarkingDepth(scene.primitives, camera, null);
-            }
-
-            this.marking.maskTarget.bind();
-            renderer.clear(false, true);
-            renderer.renderMarkingMask(scene.primitives, camera, markingDepthTest ? this.marking.depthTarget.texture : null);
+                renderer.renderMarkingMask(scene.primitives, camera, markingDepthTest ? this.marking.depthTarget.texture : null);
 
-            this.marking.update(props.marking);
-            this.marking.render(camera.viewport, postprocessingEnabled ? this.postprocessing.target : this.colorTarget);
+                this.marking.update(props.marking);
+                this.marking.render(camera.viewport, postprocessingEnabled ? this.postprocessing.target : this.colorTarget);
+            }
         }
 
         if (helper.debug.isEnabled) {

+ 26 - 6
src/mol-canvas3d/passes/multi-sample.ts

@@ -1,5 +1,5 @@
 /**
- * Copyright (c) 2019-2021 mol* contributors, licensed under MIT, See LICENSE file for more info.
+ * Copyright (c) 2019-2022 mol* contributors, licensed under MIT, See LICENSE file for more info.
  *
  * @author Alexander Rose <alexander.rose@weirdbyte.de>
  */
@@ -157,6 +157,14 @@ export class MultiSamplePass {
             ValueCell.update(compose.values.uWeight, sampleWeight);
 
             // render scene
+            if (i === 0) {
+                drawPass.postprocessing.setOcclusionOffset(0, 0);
+            } else {
+                drawPass.postprocessing.setOcclusionOffset(
+                    offset[0] / width,
+                    offset[1] / height
+                );
+            }
             drawPass.render(ctx, props, false);
 
             // compose rendered scene with compose target
@@ -175,6 +183,8 @@ export class MultiSamplePass {
             compose.render();
         }
 
+        drawPass.postprocessing.setOcclusionOffset(0, 0);
+
         ValueCell.update(compose.values.uWeight, 1.0);
         ValueCell.update(compose.values.tColor, composeTarget.texture);
         compose.update();
@@ -236,6 +246,14 @@ export class MultiSamplePass {
                 camera.update();
 
                 // render scene
+                if (sampleIndex === 0) {
+                    drawPass.postprocessing.setOcclusionOffset(0, 0);
+                } else {
+                    drawPass.postprocessing.setOcclusionOffset(
+                        offset[0] / width,
+                        offset[1] / height
+                    );
+                }
                 drawPass.render(ctx, props, false);
 
                 // compose rendered scene with compose target
@@ -258,6 +276,8 @@ export class MultiSamplePass {
             }
         }
 
+        drawPass.postprocessing.setOcclusionOffset(0, 0);
+
         this.bindOutputTarget(toDrawingBuffer);
         gl.viewport(x, y, width, height);
         gl.scissor(x, y, width, height);
@@ -291,23 +311,23 @@ const JitterVectors = [
         [0, 0]
     ],
     [
-        [4, 4], [-4, -4]
+        [0, 0], [-4, -4]
     ],
     [
-        [-2, -6], [6, -2], [-6, 2], [2, 6]
+        [0, 0], [6, -2], [-6, 2], [2, 6]
     ],
     [
-        [1, -3], [-1, 3], [5, 1], [-3, -5],
+        [0, 0], [-1, 3], [5, 1], [-3, -5],
         [-5, 5], [-7, -1], [3, 7], [7, -7]
     ],
     [
-        [1, 1], [-1, -3], [-3, 2], [4, -1],
+        [0, 0], [-1, -3], [-3, 2], [4, -1],
         [-5, -2], [2, 5], [5, 3], [3, -5],
         [-2, 6], [0, -7], [-4, -6], [-6, 4],
         [-8, 0], [7, -4], [6, 7], [-7, -8]
     ],
     [
-        [-4, -7], [-7, -5], [-3, -5], [-5, -4],
+        [0, 0], [-7, -5], [-3, -5], [-5, -4],
         [-1, -4], [-2, -2], [-6, -1], [-4, 0],
         [-7, 1], [-1, 2], [-6, 3], [-3, 3],
         [-7, 6], [-3, 6], [-5, 7], [-1, 7],

+ 60 - 21
src/mol-canvas3d/passes/postprocessing.ts

@@ -1,11 +1,11 @@
 /**
- * Copyright (c) 2019-2021 mol* contributors, licensed under MIT, See LICENSE file for more info.
+ * Copyright (c) 2019-2022 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>
  */
 
-import { QuadSchema, QuadValues } from '../../mol-gl/compute/util';
+import { CopyRenderable, createCopyRenderable, QuadSchema, QuadValues } from '../../mol-gl/compute/util';
 import { TextureSpec, Values, UniformSpec, DefineSpec } from '../../mol-gl/renderable/schema';
 import { ShaderCode } from '../../mol-gl/shader-code';
 import { WebGLContext } from '../../mol-gl/webgl/context';
@@ -199,6 +199,7 @@ const PostprocessingSchema = {
     uMaxPossibleViewZDiff: UniformSpec('f'),
 
     dOcclusionEnable: DefineSpec('boolean'),
+    uOcclusionOffset: UniformSpec('v2'),
 
     dOutlineEnable: DefineSpec('boolean'),
     dOutlineScale: DefineSpec('number'),
@@ -227,6 +228,7 @@ function getPostprocessingRenderable(ctx: WebGLContext, colorTexture: Texture, d
         uMaxPossibleViewZDiff: ValueCell.create(0.5),
 
         dOcclusionEnable: ValueCell.create(true),
+        uOcclusionOffset: ValueCell.create(Vec2.create(0, 0)),
 
         dOutlineEnable: ValueCell.create(false),
         dOutlineScale: ValueCell.create(1),
@@ -244,9 +246,10 @@ export const PostprocessingParams = {
     occlusion: PD.MappedStatic('on', {
         on: PD.Group({
             samples: PD.Numeric(32, { min: 1, max: 256, step: 1 }),
-            radius: PD.Numeric(5, { min: 0, max: 10, step: 0.1 }, { description: 'Final radius is 2^x.' }),
+            radius: PD.Numeric(5, { min: 0, max: 10, step: 0.1 }, { description: 'Final occlusion radius is 2^x' }),
             bias: PD.Numeric(0.8, { min: 0, max: 3, step: 0.1 }),
             blurKernelSize: PD.Numeric(15, { min: 1, max: 25, step: 2 }),
+            scaleFactor: PD.Numeric(1, { min: 0.1, max: 1, step: 0.05 }, { description: 'Adjust resolution of occlusion calculation' }),
         }),
         off: PD.Group({})
     }, { cycle: true, description: 'Darken occluded crevices with the ambient occlusion effect' }),
@@ -281,6 +284,9 @@ export class PostprocessingPass {
     private readonly ssaoBlurFirstPassFramebuffer: Framebuffer;
     private readonly ssaoBlurSecondPassFramebuffer: Framebuffer;
 
+    private readonly downsampledDepthTarget: RenderTarget;
+    private readonly downsampleDepthRenderable: CopyRenderable;
+
     private readonly ssaoDepthTexture: Texture;
     private readonly ssaoDepthBlurProxyTexture: Texture;
 
@@ -290,24 +296,25 @@ export class PostprocessingPass {
 
     private nSamples: number;
     private blurKernelSize: number;
+    private downsampleFactor: number;
 
     private readonly renderable: PostprocessingRenderable;
 
     private ssaoScale: number;
     private calcSsaoScale() {
         // downscale ssao for high pixel-ratios
-        return Math.min(1, 1 / this.webgl.pixelRatio);
+        return Math.min(1, 1 / this.webgl.pixelRatio) * this.downsampleFactor;
     }
 
-    constructor(private webgl: WebGLContext, drawPass: DrawPass) {
-        this.ssaoScale = this.calcSsaoScale();
-
+    constructor(private webgl: WebGLContext, private drawPass: DrawPass) {
         const { colorTarget, depthTexture } = drawPass;
         const width = colorTarget.getWidth();
         const height = colorTarget.getHeight();
 
         this.nSamples = 1;
         this.blurKernelSize = 1;
+        this.downsampleFactor = 1;
+        this.ssaoScale = this.calcSsaoScale();
 
         // needs to be linear for anti-aliasing pass
         this.target = webgl.createRenderTarget(width, height, false, 'uint8', 'linear');
@@ -332,17 +339,20 @@ export class PostprocessingPass {
         const sw = Math.floor(width * this.ssaoScale);
         const sh = Math.floor(height * this.ssaoScale);
 
-        this.ssaoDepthTexture = webgl.resources.texture('image-uint8', 'rgba', 'ubyte', 'nearest');
+        this.downsampledDepthTarget = webgl.createRenderTarget(sw, sh, false, 'uint8', 'linear');
+        this.downsampleDepthRenderable = createCopyRenderable(webgl, depthTexture);
+
+        this.ssaoDepthTexture = webgl.resources.texture('image-uint8', 'rgba', 'ubyte', 'linear');
         this.ssaoDepthTexture.define(sw, sh);
         this.ssaoDepthTexture.attachFramebuffer(this.ssaoFramebuffer, 'color0');
 
-        this.ssaoDepthBlurProxyTexture = webgl.resources.texture('image-uint8', 'rgba', 'ubyte', 'nearest');
+        this.ssaoDepthBlurProxyTexture = webgl.resources.texture('image-uint8', 'rgba', 'ubyte', 'linear');
         this.ssaoDepthBlurProxyTexture.define(sw, sh);
         this.ssaoDepthBlurProxyTexture.attachFramebuffer(this.ssaoBlurFirstPassFramebuffer, 'color0');
 
         this.ssaoDepthTexture.attachFramebuffer(this.ssaoBlurSecondPassFramebuffer, 'color0');
 
-        this.ssaoRenderable = getSsaoRenderable(webgl, depthTexture);
+        this.ssaoRenderable = getSsaoRenderable(webgl, this.downsampleFactor === 1 ? depthTexture : this.downsampledDepthTarget.texture);
         this.ssaoBlurFirstPassRenderable = getSsaoBlurRenderable(webgl, this.ssaoDepthTexture, 'horizontal');
         this.ssaoBlurSecondPassRenderable = getSsaoBlurRenderable(webgl, this.ssaoDepthBlurProxyTexture, 'vertical');
         this.renderable = getPostprocessingRenderable(webgl, colorTarget.texture, depthTexture, this.outlinesTarget.texture, this.ssaoDepthTexture);
@@ -359,11 +369,13 @@ export class PostprocessingPass {
             const sh = Math.floor(height * this.ssaoScale);
             this.target.setSize(width, height);
             this.outlinesTarget.setSize(width, height);
+            this.downsampledDepthTarget.setSize(sw, sh);
             this.ssaoDepthTexture.define(sw, sh);
             this.ssaoDepthBlurProxyTexture.define(sw, sh);
 
             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.downsampleDepthRenderable.values.uTexSize, Vec2.set(this.downsampleDepthRenderable.values.uTexSize.ref.value, sw, sh));
             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.ssaoBlurFirstPassRenderable.values.uTexSize.ref.value, sw, sh));
             ValueCell.update(this.ssaoBlurSecondPassRenderable.values.uTexSize, Vec2.set(this.ssaoBlurSecondPassRenderable.values.uTexSize.ref.value, sw, sh));
@@ -434,6 +446,30 @@ export class PostprocessingPass {
                 ValueCell.updateIfChanged(this.ssaoBlurSecondPassRenderable.values.dOcclusionKernelSize, this.blurKernelSize);
             }
 
+            if (this.downsampleFactor !== props.occlusion.params.scaleFactor) {
+                needsUpdateSsao = true;
+
+                this.downsampleFactor = props.occlusion.params.scaleFactor;
+                this.ssaoScale = this.calcSsaoScale();
+
+                const sw = Math.floor(w * this.ssaoScale);
+                const sh = Math.floor(h * this.ssaoScale);
+
+                this.downsampledDepthTarget.setSize(sw, sh);
+                this.ssaoDepthTexture.define(sw, sh);
+                this.ssaoDepthBlurProxyTexture.define(sw, sh);
+
+                if (this.ssaoScale === 1) {
+                    ValueCell.update(this.ssaoRenderable.values.tDepth, this.drawPass.depthTexture);
+                } else {
+                    ValueCell.update(this.ssaoRenderable.values.tDepth, this.downsampledDepthTarget.texture);
+                }
+
+                ValueCell.update(this.downsampleDepthRenderable.values.uTexSize, Vec2.set(this.downsampleDepthRenderable.values.uTexSize.ref.value, sw, sh));
+                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.ssaoBlurFirstPassRenderable.values.uTexSize.ref.value, sw, sh));
+                ValueCell.update(this.ssaoBlurSecondPassRenderable.values.uTexSize, Vec2.set(this.ssaoBlurSecondPassRenderable.values.uTexSize.ref.value, sw, sh));
+            }
         }
 
         if (props.outline.name === 'on') {
@@ -494,6 +530,13 @@ export class PostprocessingPass {
         gl.scissor(x, y, width, height);
     }
 
+    private occlusionOffset: [x: number, y: number] = [0, 0];
+    setOcclusionOffset(x: number, y: number) {
+        this.occlusionOffset[0] = x;
+        this.occlusionOffset[1] = y;
+        ValueCell.update(this.renderable.values.uOcclusionOffset, Vec2.set(this.renderable.values.uOcclusionOffset.ref.value, x, y));
+    }
+
     render(camera: ICamera, toDrawingBuffer: boolean, transparentBackground: boolean, backgroundColor: Color, props: PostprocessingProps) {
         this.updateState(camera, transparentBackground, backgroundColor, props);
 
@@ -502,14 +545,13 @@ export class PostprocessingPass {
             this.outlinesRenderable.render();
         }
 
-        if (props.occlusion.name === 'on') {
-            const { x, y, width, height } = camera.viewport;
-            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);
+        // don't render occlusion if offset is given,
+        // which will reuse the existing occlusion
+        if (props.occlusion.name === 'on' && this.occlusionOffset[0] === 0 && this.occlusionOffset[1] === 0) {
+            if (this.ssaoScale < 1) {
+                this.downsampledDepthTarget.bind();
+                this.downsampleDepthRenderable.render();
+            }
 
             this.ssaoFramebuffer.bind();
             this.ssaoRenderable.render();
@@ -519,9 +561,6 @@ export class PostprocessingPass {
 
             this.ssaoBlurSecondPassFramebuffer.bind();
             this.ssaoBlurSecondPassRenderable.render();
-
-            this.webgl.gl.viewport(x, y, width, height);
-            this.webgl.gl.scissor(x, y, width, height);
         }
 
         if (toDrawingBuffer) {

+ 14 - 2
src/mol-gl/scene.ts

@@ -1,5 +1,5 @@
 /**
- * Copyright (c) 2018-2021 mol* contributors, licensed under MIT, See LICENSE file for more info.
+ * Copyright (c) 2018-2022 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>
@@ -79,6 +79,7 @@ interface Scene extends Object3D {
     has: (o: GraphicsRenderObject) => boolean
     clear: () => void
     forEach: (callbackFn: (value: GraphicsRenderable, key: GraphicsRenderObject) => void) => void
+    getMarkerAverage: () => number
 }
 
 namespace Scene {
@@ -243,7 +244,18 @@ namespace Scene {
                     visibleHash = computeVisibleHash();
                 }
                 return boundingSphereVisible;
-            }
+            },
+            getMarkerAverage() {
+                if (primitives.length === 0 && volumes.length === 0) return 0;
+                let markerAverage = 0;
+                for (let i = 0, il = primitives.length; i < il; ++i) {
+                    markerAverage += primitives[i].values.markerAverage.ref.value;
+                }
+                for (let i = 0, il = volumes.length; i < il; ++i) {
+                    markerAverage += volumes[i].values.markerAverage.ref.value;
+                }
+                return markerAverage / (primitives.length + volumes.length);
+            },
         };
     }
 }

+ 3 - 4
src/mol-gl/shader/postprocessing.frag.ts

@@ -1,5 +1,5 @@
 /**
- * Copyright (c) 2019-2021 mol* contributors, licensed under MIT, See LICENSE file for more info.
+ * Copyright (c) 2019-2022 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>
@@ -24,8 +24,7 @@ uniform vec3 uFogColor;
 uniform vec3 uOutlineColor;
 uniform bool uTransparentBackground;
 
-uniform float uOcclusionBias;
-uniform float uOcclusionRadius;
+uniform vec2 uOcclusionOffset;
 
 uniform float uMaxPossibleViewZDiff;
 
@@ -102,7 +101,7 @@ void main(void) {
         if (!isBackground(depth)) {
             viewDist = abs(getViewZ(depth));
             fogFactor = smoothstep(uFogNear, uFogFar, viewDist);
-            float occlusionFactor = getSsao(coords);
+            float occlusionFactor = getSsao(coords + uOcclusionOffset);
             if (!uTransparentBackground) {
                 color.rgb = mix(mix(occlusionColor, uFogColor, fogFactor), color.rgb, occlusionFactor);
             } else {

+ 28 - 0
src/mol-io/common/_spec/encoder.spec.ts

@@ -0,0 +1,28 @@
+import { ArrayEncoding } from '../binary-cif/array-encoder';
+import { decode } from '../binary-cif/decoder';
+
+const E = ArrayEncoding;
+
+test('fixedPoint2', async () => {
+    const fixedPoint2 = E.by(E.fixedPoint(100)).and(E.delta).and(E.integerPacking);
+
+    const x = [1.092, 1.960, 0.666, 0.480, 1.267];
+    const y = [7.428, 7.026, 6.851, 7.524, 8.333];
+    const z = [26.270, 26.561, 25.573, 27.055, 25.881];
+
+    const xEnc = fixedPoint2.encode(new Float32Array(x));
+    const yEnc = fixedPoint2.encode(new Float32Array(y));
+    const zEnc = fixedPoint2.encode(new Float32Array(z));
+
+    expect(xEnc.data.length).toEqual(6);
+    expect(yEnc.data.length).toEqual(5);
+    expect(zEnc.data.length).toEqual(6);
+
+    const xDec = decode(xEnc);
+    const yDec = decode(yEnc);
+    const zDec = decode(zEnc);
+
+    x.forEach((a, i) => expect(xDec[i]).toBeCloseTo(a, 2));
+    y.forEach((a, i) => expect(yDec[i]).toBeCloseTo(a, 2));
+    z.forEach((a, i) => expect(zDec[i]).toBeCloseTo(a, 2));
+});

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

@@ -103,11 +103,11 @@ async function getModels(mol2: Mol2File, ctx: RuntimeContext) {
             const flag = Column.ofIntArray(Column.mapToArray(bonds.bond_type, x => {
                 switch (x) {
                     case 'ar': // aromatic
+                    case 'am': // amide
                         return BondType.Flag.Aromatic | BondType.Flag.Covalent;
                     case 'du': // dummy
                     case 'nc': // not connected
                         return BondType.Flag.None;
-                    case 'am': // amide
                     case 'un': // unknown
                     default:
                         return BondType.Flag.Covalent;

+ 7 - 9
src/mol-model-formats/structure/property/bonds/chem_comp.ts

@@ -1,5 +1,5 @@
 /**
- * Copyright (c) 2017-2020 Mol* contributors, licensed under MIT, See LICENSE file for more info.
+ * Copyright (c) 2017-2022 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>
@@ -56,10 +56,10 @@ export namespace ComponentBond {
         const entries: Map<string, Entry> = new Map();
 
         function addEntry(id: string) {
-            // weird behavior when 'PRO' is requested - will report a single bond between N and H because a later operation would override real content
-            if (entries.has(id)) {
-                return entries.get(id)!;
-            }
+            // weird behavior when 'PRO' is requested - will report a single bond
+            // between N and H because a later operation would override real content
+            if (entries.has(id)) return entries.get(id)!;
+
             const e = new Entry(id);
             entries.set(id, e);
             return e;
@@ -83,10 +83,8 @@ export namespace ComponentBond {
             let ord = 1;
             if (aromatic) flags |= BondType.Flag.Aromatic;
             switch (order.toLowerCase()) {
-                case 'doub':
-                case 'delo':
-                    ord = 2;
-                    break;
+                case 'delo': flags |= BondType.Flag.Aromatic; break;
+                case 'doub': ord = 2; break;
                 case 'trip': ord = 3; break;
                 case 'quad': ord = 4; break;
             }

+ 8 - 0
src/mol-model/structure/structure/unit.ts

@@ -25,6 +25,7 @@ import { Mat4, Vec3 } from '../../../mol-math/linear-algebra';
 import { IndexPairBonds } from '../../../mol-model-formats/structure/property/bonds/index-pair';
 import { ElementSetIntraBondCache } from './unit/bonds/element-set-intra-bond-cache';
 import { ModelSymmetry } from '../../../mol-model-formats/structure/property/symmetry';
+import { getResonance, UnitResonance } from './unit/resonance';
 
 // avoiding namespace lookup improved performance in Chrome (Aug 2020)
 const v3add = Vec3.add;
@@ -308,6 +309,12 @@ namespace Unit {
             return this.props.rings;
         }
 
+        get resonance() {
+            if (this.props.resonance) return this.props.resonance;
+            this.props.resonance = getResonance(this);
+            return this.props.resonance;
+        }
+
         get polymerElements() {
             if (this.props.polymerElements) return this.props.polymerElements;
             this.props.polymerElements = getAtomicPolymerElements(this);
@@ -368,6 +375,7 @@ namespace Unit {
     interface AtomicProperties extends BaseProperties {
         bonds?: IntraUnitBonds
         rings?: UnitRings
+        resonance?: UnitResonance
         nucleotideElements?: SortedArray<ElementIndex>
         proteinElements?: SortedArray<ElementIndex>
         residueCount?: number

+ 83 - 0
src/mol-model/structure/structure/unit/resonance.ts

@@ -0,0 +1,83 @@
+/**
+ * Copyright (c) 2022 mol* contributors, licensed under MIT, See LICENSE file for more info.
+ *
+ * @author Alexander Rose <alexander.rose@weirdbyte.de>
+ */
+
+import { SortedArray } from '../../../../mol-data/int/sorted-array';
+import { sortedCantorPairing } from '../../../../mol-data/util';
+import { BondType } from '../../model/types';
+import { StructureElement } from '../element';
+import { Unit } from '../unit';
+
+export type UnitResonance = {
+    /**
+     * Lookup for triplets of atoms in delocalized bonds.
+     *
+     * Does not include triplets that are part of aromatic rings.
+     */
+    readonly delocalizedTriplets: {
+        /** Return 3rd element in triplet or undefined if `a` and `b` are not part of a triplet */
+        readonly getThirdElement: (a: StructureElement.UnitIndex, b: StructureElement.UnitIndex) => StructureElement.UnitIndex | undefined
+        /** Return index into `triplets` or undefined if `a` is not part of any triplet */
+        readonly getTripletIndices: (a: StructureElement.UnitIndex) => number[] | undefined
+        readonly triplets: SortedArray<StructureElement.UnitIndex>[]
+    }
+}
+
+export function getResonance(unit: Unit.Atomic): UnitResonance {
+    return {
+        delocalizedTriplets: getDelocalizedTriplets(unit)
+    };
+}
+
+function getDelocalizedTriplets(unit: Unit.Atomic) {
+    const bonds = unit.bonds;
+    const { b, edgeProps, offset } = bonds;
+    const { order: _order, flags: _flags } = edgeProps;
+    const { elementAromaticRingIndices } = unit.rings;
+
+    const triplets: SortedArray<StructureElement.UnitIndex>[] = [];
+    const thirdElementMap = new Map<number, StructureElement.UnitIndex>();
+    const indicesMap = new Map<number, number[]>();
+
+    const add = (a: StructureElement.UnitIndex, b: StructureElement.UnitIndex, c: StructureElement.UnitIndex) => {
+        const index = triplets.length;
+        triplets.push(SortedArray.ofUnsortedArray([a, b, c]));
+        thirdElementMap.set(sortedCantorPairing(a, b), c);
+        if (indicesMap.has(a)) indicesMap.get(a)!.push(index);
+        else indicesMap.set(a, [index]);
+    };
+
+    for (let i = 0 as StructureElement.UnitIndex; i < unit.elements.length; i++) {
+        if (elementAromaticRingIndices.has(i)) continue;
+
+        const count = offset[i + 1] - offset[i] + 1;
+        if (count < 2) continue;
+
+        const deloBonds: StructureElement.UnitIndex[] = [];
+        for (let t = offset[i], _t = offset[i + 1]; t < _t; t++) {
+            const f = _flags[t];
+            if (!BondType.is(f, BondType.Flag.Aromatic)) continue;
+
+            deloBonds.push(b[t]);
+        }
+
+        if (deloBonds.length >= 2) {
+            add(i, deloBonds[0], deloBonds[1]);
+            for (let j = 1, jl = deloBonds.length; j < jl; j++) {
+                add(i, deloBonds[j], deloBonds[0]);
+            }
+        }
+    }
+
+    return {
+        getThirdElement: (a: StructureElement.UnitIndex, b: StructureElement.UnitIndex) => {
+            return thirdElementMap.get(sortedCantorPairing(a, b));
+        },
+        getTripletIndices: (a: StructureElement.UnitIndex) => {
+            return indicesMap.get(a);
+        },
+        triplets,
+    };
+}

+ 70 - 39
src/mol-model/structure/structure/unit/rings/compute.ts

@@ -28,17 +28,19 @@ export function computeRings(unit: Unit.Atomic) {
 }
 
 const enum Constants {
-    MaxDepth = 4
+    MaxDepth = 5
 }
 
 interface State {
     startVertex: number,
     endVertex: number,
     count: number,
-    visited: Int32Array,
+    isRingAtom: Int32Array,
+    marked: Int32Array,
     queue: Int32Array,
     color: Int32Array,
     pred: Int32Array,
+    depth: Int32Array,
 
     left: Int32Array,
     right: Int32Array,
@@ -59,9 +61,11 @@ function State(unit: Unit.Atomic, capacity: number): State {
         startVertex: 0,
         endVertex: 0,
         count: 0,
-        visited: new Int32Array(capacity),
+        isRingAtom: new Int32Array(capacity),
+        marked: new Int32Array(capacity),
         queue: new Int32Array(capacity),
         pred: new Int32Array(capacity),
+        depth: new Int32Array(capacity),
         left: new Int32Array(Constants.MaxDepth),
         right: new Int32Array(Constants.MaxDepth),
         color: new Int32Array(capacity),
@@ -78,17 +82,26 @@ function State(unit: Unit.Atomic, capacity: number): State {
 
 function resetState(state: State) {
     state.count = state.endVertex - state.startVertex;
-    const { visited, pred, color } = state;
+    const { isRingAtom, pred, color, depth, marked } = state;
     for (let i = 0; i < state.count; i++) {
-        visited[i] = -1;
+        isRingAtom[i] = 0;
         pred[i] = -1;
+        marked[i] = -1;
         color[i] = 0;
+        depth[i] = 0;
     }
     state.currentColor = 0;
     state.currentAltLoc = '';
     state.hasAltLoc = false;
 }
 
+function resetDepth(state: State) {
+    const { depth } = state;
+    for (let i = 0; i < state.count; i++) {
+        depth[i] = state.count + 1;
+    }
+}
+
 function largestResidue(unit: Unit.Atomic) {
     const residuesIt = Segmentation.transientSegments(unit.model.atomicHierarchy.residueAtomSegments, unit.elements);
     let size = 0;
@@ -99,8 +112,16 @@ function largestResidue(unit: Unit.Atomic) {
     return size;
 }
 
+function isStartIndex(state: State, i: number) {
+    const bondOffset = state.bonds.offset;
+    const a = state.startVertex + i;
+    const bStart = bondOffset[a], bEnd = bondOffset[a + 1];
+    const bondCount = bEnd - bStart;
+    if (bondCount <= 1 || (state.isRingAtom[i] && bondCount === 2)) return false;
+    return true;
+}
+
 function processResidue(state: State, start: number, end: number) {
-    const { visited } = state;
     state.startVertex = start;
     state.endVertex = end;
 
@@ -117,11 +138,13 @@ function processResidue(state: State, start: number, end: number) {
     }
     arraySetRemove(altLocs, '');
 
+    let mark = 1;
     if (altLocs.length === 0) {
         resetState(state);
         for (let i = 0; i < state.count; i++) {
-            if (visited[i] >= 0) continue;
-            findRings(state, i);
+            if (!isStartIndex(state, i)) continue;
+            resetDepth(state);
+            mark = findRings(state, i, mark);
         }
     } else {
         for (let aI = 0; aI < altLocs.length; aI++) {
@@ -129,12 +152,13 @@ function processResidue(state: State, start: number, end: number) {
             state.hasAltLoc = true;
             state.currentAltLoc = altLocs[aI];
             for (let i = 0; i < state.count; i++) {
-                if (visited[i] >= 0) continue;
+                if (!isStartIndex(state, i)) continue;
                 const altLoc = state.altLoc.value(elements[state.startVertex + i]);
                 if (altLoc && altLoc !== state.currentAltLoc) {
                     continue;
                 }
-                findRings(state, i);
+                resetDepth(state);
+                mark = findRings(state, i, mark);
             }
         }
     }
@@ -144,10 +168,10 @@ function processResidue(state: State, start: number, end: number) {
     }
 }
 
-function addRing(state: State, a: number, b: number) {
+function addRing(state: State, a: number, b: number, isRingAtom: Int32Array) {
     // only "monotonous" rings
     if (b < a) {
-        return;
+        return false;
     }
 
     const { pred, color, left, right } = state;
@@ -176,7 +200,7 @@ function addRing(state: State, a: number, b: number) {
         if (current < 0) break;
     }
     if (!found) {
-        return;
+        return false;
     }
 
     current = a;
@@ -190,50 +214,50 @@ function addRing(state: State, a: number, b: number) {
     const len = leftOffset + rightOffset;
     // rings must have at least three elements
     if (len < 3) {
-        return;
+        return false;
     }
 
     const ring = new Int32Array(len);
     let ringOffset = 0;
-    for (let t = 0; t < leftOffset; t++) ring[ringOffset++] = state.startVertex + left[t];
-    for (let t = rightOffset - 1; t >= 0; t--) ring[ringOffset++] = state.startVertex + right[t];
+    for (let t = 0; t < leftOffset; t++) {
+        ring[ringOffset++] = state.startVertex + left[t];
+        isRingAtom[left[t]] = 1;
+    }
+    for (let t = rightOffset - 1; t >= 0; t--) {
+        ring[ringOffset++] = state.startVertex + right[t];
+        isRingAtom[right[t]] = 1;
+    }
 
     sortArray(ring);
 
-    if (state.hasAltLoc) {
-        // we need to check if the ring was already added because alt locs are present.
-
-        for (let rI = 0, _rI = state.currentRings.length; rI < _rI; rI++) {
-            const r = state.currentRings[rI];
-            if (ring[0] !== r[0]) continue;
-            if (ring.length !== r.length) continue;
+    // Check if the ring is unique and another one is not it's subset
+    for (let rI = 0, _rI = state.currentRings.length; rI < _rI; rI++) {
+        const r = state.currentRings[rI];
 
-            let areSame = true;
-            for (let aI = 0, _aI = ring.length; aI < _aI; aI++) {
-                if (ring[aI] !== r[aI]) {
-                    areSame = false;
-                    break;
-                }
-            }
-            if (areSame) {
-                return;
-            }
+        if (ring.length === r.length) {
+            if (SortedArray.areEqual(ring as any, r)) return false;
+        } else if (ring.length > r.length) {
+            if (SortedArray.isSubset(ring as any, r)) return false;
         }
     }
 
     state.currentRings.push(SortedArray.ofSortedArray(ring));
+
+    return true;
 }
 
-function findRings(state: State, from: number) {
-    const { bonds, startVertex, endVertex, visited, queue, pred } = state;
+function findRings(state: State, from: number, mark: number) {
+    const { bonds, startVertex, endVertex, isRingAtom, marked, queue, pred, depth } = state;
     const { elements } = state.unit;
     const { b: neighbor, edgeProps: { flags: bondFlags }, offset } = bonds;
-    visited[from] = 1;
+    marked[from] = mark;
+    depth[from] = 0;
     queue[0] = from;
     let head = 0, size = 1;
 
     while (head < size) {
         const top = queue[head++];
+        const d = depth[top];
         const a = startVertex + top;
         const start = offset[a], end = offset[a + 1];
 
@@ -250,18 +274,25 @@ function findRings(state: State, from: number) {
 
             const other = b - startVertex;
 
-            if (visited[other] > 0) {
+            if (marked[other] === mark) {
                 if (pred[other] !== top && pred[top] !== other) {
-                    addRing(state, top, other);
+                    if (addRing(state, top, other, isRingAtom)) {
+                        return mark + 1;
+                    }
                 }
                 continue;
             }
 
-            visited[other] = 1;
+            const newDepth = Math.min(depth[other], d + 1);
+            if (newDepth > Constants.MaxDepth) continue;
+
+            depth[other] = newDepth;
+            marked[other] = mark;
             queue[size++] = other;
             pred[other] = top;
         }
     }
+    return mark + 1;
 }
 
 export function getFingerprint(elements: string[]) {

+ 2 - 2
src/mol-plugin-ui/structure/quick-styles.tsx

@@ -60,7 +60,7 @@ export class QuickStyles extends PurePluginUIComponent {
                     },
                     occlusion: {
                         name: 'on',
-                        params: { bias: 0.9, blurKernelSize: 15, radius: 5, samples: 32 }
+                        params: { bias: 0.8, blurKernelSize: 15, radius: 5, samples: 32, scaleFactor: 1 }
                     },
                 }
             });
@@ -84,7 +84,7 @@ export class QuickStyles extends PurePluginUIComponent {
                         name: 'on',
                         params: pp.occlusion.name === 'on'
                             ? pp.occlusion.params
-                            : { bias: 0.9, blurKernelSize: 15, radius: 5, samples: 32 }
+                            : { bias: 0.8, blurKernelSize: 15, radius: 5, samples: 32, scaleFactor: 1 }
                     },
                 }
             });

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

@@ -119,7 +119,7 @@ class ViewportScreenshotHelper extends PluginComponent {
             postprocessing: {
                 ...c.props.postprocessing,
                 occlusion: aoProps.name === 'on'
-                    ? { name: 'on', params: { ...aoProps.params, samples: 128 } }
+                    ? { name: 'on', params: { ...aoProps.params, samples: 128, scaleFactor: 1 } }
                     : aoProps
             },
             marking: { ...c.props.marking }
@@ -143,7 +143,7 @@ class ViewportScreenshotHelper extends PluginComponent {
                 postprocessing: {
                     ...c.props.postprocessing,
                     occlusion: aoProps.name === 'on'
-                        ? { name: 'on', params: { ...aoProps.params, samples: 128 } }
+                        ? { name: 'on', params: { ...aoProps.params, samples: 128, scaleFactor: 1 } }
                         : aoProps
                 },
                 marking: { ...c.props.marking }

+ 8 - 2
src/mol-repr/structure/visual/bond-intra-unit-cylinder.ts

@@ -1,5 +1,5 @@
 /**
- * Copyright (c) 2018-2021 mol* contributors, licensed under MIT, See LICENSE file for more info.
+ * Copyright (c) 2018-2022 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>
@@ -79,12 +79,16 @@ function getIntraUnitBondCylinderBuilderProps(unit: Unit.Atomic, structure: Stru
     };
 
     const { elementRingIndices, elementAromaticRingIndices } = unit.rings;
+    const deloTriplets = aromaticBonds ? unit.resonance.delocalizedTriplets : undefined;
 
     return {
         linkCount: edgeCount * 2,
         referencePosition: (edgeIndex: number) => {
             let aI = a[edgeIndex], bI = b[edgeIndex];
 
+            const rI = deloTriplets?.getThirdElement(aI, bI);
+            if (rI !== undefined) return pos(elements[rI], vRef);
+
             if (aI > bI) [aI, bI] = [bI, aI];
             if (offset[aI + 1] - offset[aI] === 1) [aI, bI] = [bI, aI];
 
@@ -145,8 +149,10 @@ function getIntraUnitBondCylinderBuilderProps(unit: Unit.Atomic, structure: Stru
                 if (isBondType(f, BondType.Flag.Aromatic) || (arCount && !ignoreComputedAromatic)) {
                     if (arCount === 2) {
                         return LinkStyle.MirroredAromatic;
-                    } else {
+                    } else if (arCount === 1 || deloTriplets?.getThirdElement(aI, bI)) {
                         return LinkStyle.Aromatic;
+                    } else {
+                        // case for bonds between two aromatic rings
                     }
                 }
             }

+ 8 - 2
src/mol-repr/structure/visual/bond-intra-unit-line.ts

@@ -1,5 +1,5 @@
 /**
- * Copyright (c) 2020-2021 mol* contributors, licensed under MIT, See LICENSE file for more info.
+ * Copyright (c) 2020-2022 mol* contributors, licensed under MIT, See LICENSE file for more info.
  *
  * @author Alexander Rose <alexander.rose@weirdbyte.de>
  */
@@ -52,12 +52,16 @@ function createIntraUnitBondLines(ctx: VisualContext, unit: Unit, structure: Str
     const pos = unit.conformation.invariantPosition;
 
     const { elementRingIndices, elementAromaticRingIndices } = unit.rings;
+    const deloTriplets = aromaticBonds ? unit.resonance.delocalizedTriplets : undefined;
 
     const builderProps: LinkBuilderProps = {
         linkCount: edgeCount * 2,
         referencePosition: (edgeIndex: number) => {
             let aI = a[edgeIndex], bI = b[edgeIndex];
 
+            const rI = deloTriplets?.getThirdElement(aI, bI);
+            if (rI !== undefined) return pos(elements[rI], vRef);
+
             if (aI > bI) [aI, bI] = [bI, aI];
             if (offset[aI + 1] - offset[aI] === 1) [aI, bI] = [bI, aI];
 
@@ -106,8 +110,10 @@ function createIntraUnitBondLines(ctx: VisualContext, unit: Unit, structure: Str
                 if (isBondType(f, BondType.Flag.Aromatic) || (arCount && !ignoreComputedAromatic)) {
                     if (arCount === 2) {
                         return LinkStyle.MirroredAromatic;
-                    } else {
+                    } else if (arCount === 1 || deloTriplets?.getThirdElement(aI, bI)) {
                         return LinkStyle.Aromatic;
+                    } else {
+                        // case for bonds between two aromatic rings
                     }
                 }
             }

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

@@ -264,4 +264,4 @@ export function eachInterBond(loci: Loci, structure: Structure, apply: (interval
         __unitMap.clear();
     }
     return changed;
-}
+}