Browse Source

Merge pull request #221 from molstar/aromatic

Aromatic bond display option
Alexander Rose 3 years ago
parent
commit
9b56a6ae65

+ 1 - 0
CHANGELOG.md

@@ -6,6 +6,7 @@ Note that since we don't clearly distinguish between a public and private interf
 
 ## [Unreleased]
 
+- Add parameter for to display aromatic bonds as dashes next to solid cylinder/line.
 - Add backbone representation
 - Fix outline in orthographic mode and set default scale to 2.
 

+ 28 - 3
src/mol-model-formats/structure/mol2.ts

@@ -6,7 +6,7 @@
 
 import { Column, Table } from '../../mol-data/db';
 import { Model } from '../../mol-model/structure/model';
-import { MoleculeType } from '../../mol-model/structure/model/types';
+import { BondType, MoleculeType } from '../../mol-model/structure/model/types';
 import { RuntimeContext, Task } from '../../mol-task';
 import { createModels } from './basic/parser';
 import { BasicSchema, createBasic } from './basic/schema';
@@ -74,8 +74,33 @@ async function getModels(mol2: Mol2File, ctx: RuntimeContext) {
         if (_models.frameCount > 0) {
             const indexA = Column.ofIntArray(Column.mapToArray(bonds.origin_atom_id, x => x - 1, Int32Array));
             const indexB = Column.ofIntArray(Column.mapToArray(bonds.target_atom_id, x => x - 1, Int32Array));
-            const order = Column.ofIntArray(Column.mapToArray(bonds.bond_type, x => x === 'ar' ? 1 : parseInt(x), Int8Array));
-            const pairBonds = IndexPairBonds.fromData({ pairs: { indexA, indexB, order }, count: atoms.count });
+            const order = Column.ofIntArray(Column.mapToArray(bonds.bond_type, x => {
+                switch (x) {
+                    case 'ar': // aromatic
+                    case 'am': // amide
+                    case 'un': // unknown
+                        return 1;
+                    case 'du': // dummy
+                    case 'nc': // not connected
+                        return 0;
+                    default:
+                        return parseInt(x);
+                }
+            }, Int8Array));
+            const flag = Column.ofIntArray(Column.mapToArray(bonds.bond_type, x => {
+                switch (x) {
+                    case 'ar': // aromatic
+                        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;
+                }
+            }, Int8Array));
+            const pairBonds = IndexPairBonds.fromData({ pairs: { indexA, indexB, order, flag }, count: atoms.count });
 
             const first = _models.representative;
             IndexPairBonds.Provider.set(first, pairBonds);

+ 3 - 3
src/mol-model/structure/model/types.ts

@@ -620,9 +620,9 @@ export namespace BondType {
         'covalent': Flag.Covalent,
         'metal-coordination': Flag.MetallicCoordination,
         'hydrogen-bond': Flag.HydrogenBond,
-        'disulfide': Flag.HydrogenBond,
-        'aromatic': Flag.HydrogenBond,
-        'computed': Flag.HydrogenBond,
+        'disulfide': Flag.Disulfide,
+        'aromatic': Flag.Aromatic,
+        'computed': Flag.Computed,
     };
     export type Names = keyof typeof Names
 

+ 0 - 1
src/mol-model/structure/structure/unit/rings.ts

@@ -117,7 +117,6 @@ namespace UnitRing {
                 // comes e.g. from `chem_comp_bond.pdbx_aromatic_flag`
                 if (BondType.is(BondType.Flag.Aromatic, flags[j])) {
                     if (SortedArray.has(ring, b[j])) aromaticBondCount += 1;
-
                 }
             }
         }

+ 0 - 1
src/mol-repr/structure/visual/aromatic-ring-mesh.ts

@@ -1 +0,0 @@
-// TODO

+ 7 - 5
src/mol-repr/structure/visual/bond-inter-unit-cylinder.ts

@@ -40,7 +40,7 @@ function getInterUnitBondCylinderBuilderProps(structure: Structure, theme: Theme
 
     const bonds = structure.interUnitBonds;
     const { edgeCount, edges } = bonds;
-    const { sizeFactor, sizeAspectRatio, adjustCylinderLength } = props;
+    const { sizeFactor, sizeAspectRatio, adjustCylinderLength, aromaticBonds } = props;
 
     const delta = Vec3();
 
@@ -135,13 +135,15 @@ function getInterUnitBondCylinderBuilderProps(structure: Structure, theme: Theme
             if (BondType.is(f, BondType.Flag.MetallicCoordination) || BondType.is(f, BondType.Flag.HydrogenBond)) {
                 // show metall coordinations and hydrogen bonds with dashed cylinders
                 return LinkStyle.Dashed;
-            } else if (o === 2) {
-                return LinkStyle.Double;
             } else if (o === 3) {
                 return LinkStyle.Triple;
-            } else {
-                return LinkStyle.Solid;
+            } else if (aromaticBonds && BondType.is(f, BondType.Flag.Aromatic)) {
+                return LinkStyle.Aromatic;
+            } else if (o === 2) {
+                return LinkStyle.Double;
             }
+
+            return LinkStyle.Solid;
         },
         radius: (edgeIndex: number) => {
             return radius(edgeIndex) * sizeAspectRatio;

+ 7 - 5
src/mol-repr/structure/visual/bond-inter-unit-line.ts

@@ -32,7 +32,7 @@ function setRefPosition(pos: Vec3, structure: Structure, unit: Unit.Atomic, inde
 function createInterUnitBondLines(ctx: VisualContext, structure: Structure, theme: Theme, props: PD.Values<InterUnitBondLineParams>, lines?: Lines) {
     const bonds = structure.interUnitBonds;
     const { edgeCount, edges } = bonds;
-    const { sizeFactor } = props;
+    const { sizeFactor, aromaticBonds } = props;
 
     if (!edgeCount) return Lines.createEmpty(lines);
 
@@ -73,13 +73,15 @@ function createInterUnitBondLines(ctx: VisualContext, structure: Structure, them
             if (BondType.is(f, BondType.Flag.MetallicCoordination) || BondType.is(f, BondType.Flag.HydrogenBond)) {
                 // show metall coordinations and hydrogen bonds with dashed cylinders
                 return LinkStyle.Dashed;
-            } else if (o === 2) {
-                return LinkStyle.Double;
             } else if (o === 3) {
                 return LinkStyle.Triple;
-            } else {
-                return LinkStyle.Solid;
+            }else if (aromaticBonds && BondType.is(f, BondType.Flag.Aromatic)) {
+                return LinkStyle.Aromatic;
+            } else if (o === 2) {
+                return LinkStyle.Double;
             }
+
+            return LinkStyle.Solid;
         },
         radius: (edgeIndex: number) => {
             const b = edges[edgeIndex];

+ 41 - 14
src/mol-repr/structure/visual/bond-intra-unit-cylinder.ts

@@ -22,6 +22,7 @@ import { IntAdjacencyGraph } from '../../../mol-math/graph';
 import { WebGLContext } from '../../../mol-gl/webgl/context';
 import { Cylinders } from '../../../mol-geo/geometry/cylinders/cylinders';
 import { SortedArray } from '../../../mol-data/int';
+import { arrayIntersectionSize } from '../../../mol-util/array';
 
 // avoiding namespace lookup improved performance in Chrome (Aug 2020)
 const isBondType = BondType.is;
@@ -31,7 +32,7 @@ function getIntraUnitBondCylinderBuilderProps(unit: Unit.Atomic, structure: Stru
     const bonds = unit.bonds;
     const { edgeCount, a, b, edgeProps, offset } = bonds;
     const { order: _order, flags: _flags } = edgeProps;
-    const { sizeFactor, sizeAspectRatio, adjustCylinderLength } = props;
+    const { sizeFactor, sizeAspectRatio, adjustCylinderLength, aromaticBonds } = props;
 
     const vRef = Vec3(), delta = Vec3();
     const pos = unit.conformation.invariantPosition;
@@ -69,6 +70,8 @@ function getIntraUnitBondCylinderBuilderProps(unit: Unit.Atomic, structure: Stru
         return theme.size.size(locE) * sizeFactor;
     };
 
+    const { elementRingIndices, elementAromaticRingIndices } = unit.rings;
+
     return {
         linkCount: edgeCount * 2,
         referencePosition: (edgeIndex: number) => {
@@ -76,17 +79,28 @@ function getIntraUnitBondCylinderBuilderProps(unit: Unit.Atomic, structure: Stru
 
             if (aI > bI) [aI, bI] = [bI, aI];
             if (offset[aI + 1] - offset[aI] === 1) [aI, bI] = [bI, aI];
-            // TODO prefer reference atoms within rings
+
+            const aR = elementRingIndices.get(aI);
+            let maxSize = 0;
 
             for (let i = offset[aI], il = offset[aI + 1]; i < il; ++i) {
                 const _bI = b[i];
-                if (_bI !== bI && _bI !== aI) return pos(elements[_bI], vRef);
-            }
-            for (let i = offset[bI], il = offset[bI + 1]; i < il; ++i) {
-                const _aI = a[i];
-                if (_aI !== aI && _aI !== bI) return pos(elements[_aI], vRef);
+                if (_bI !== bI && _bI !== aI) {
+                    if (aR) {
+                        const _bR = elementRingIndices.get(_bI);
+                        if (!_bR) continue;
+
+                        const size = arrayIntersectionSize(aR, _bR);
+                        if (size > maxSize) {
+                            maxSize = size;
+                            pos(elements[_bI], vRef);
+                        }
+                    } else {
+                        return pos(elements[_bI], vRef);
+                    }
+                }
             }
-            return null;
+            return maxSize > 0 ? vRef : null;
         },
         position: (posA: Vec3, posB: Vec3, edgeIndex: number) => {
             pos(elements[a[edgeIndex]], posA);
@@ -110,13 +124,24 @@ function getIntraUnitBondCylinderBuilderProps(unit: Unit.Atomic, structure: Stru
             if (isBondType(f, BondType.Flag.MetallicCoordination) || isBondType(f, BondType.Flag.HydrogenBond)) {
                 // show metall coordinations and hydrogen bonds with dashed cylinders
                 return LinkStyle.Dashed;
-            } else if (o === 2) {
-                return LinkStyle.Double;
             } else if (o === 3) {
                 return LinkStyle.Triple;
-            } else {
-                return LinkStyle.Solid;
+            } else if (aromaticBonds) {
+                const aI = a[edgeIndex], bI = b[edgeIndex];
+                const aR = elementAromaticRingIndices.get(aI);
+                const bR = elementAromaticRingIndices.get(bI);
+                const arCount = (aR && bR) ? arrayIntersectionSize(aR, bR) : 0;
+
+                if (arCount || isBondType(f, BondType.Flag.Aromatic)) {
+                    if (arCount === 2) {
+                        return LinkStyle.MirroredAromatic;
+                    } else {
+                        return LinkStyle.Aromatic;
+                    }
+                }
             }
+
+            return o === 2 ? LinkStyle.Double : LinkStyle.Solid;
         },
         radius: (edgeIndex: number) => {
             return radius(edgeIndex) * sizeAspectRatio;
@@ -197,7 +222,8 @@ export function IntraUnitBondCylinderImpostorVisual(materialId: number): UnitsVi
                 newProps.stubCap !== currentProps.stubCap ||
                 !arrayEqual(newProps.includeTypes, currentProps.includeTypes) ||
                 !arrayEqual(newProps.excludeTypes, currentProps.excludeTypes) ||
-                newProps.adjustCylinderLength !== currentProps.adjustCylinderLength
+                newProps.adjustCylinderLength !== currentProps.adjustCylinderLength ||
+                newProps.aromaticBonds !== currentProps.aromaticBonds
             );
 
             const newUnit = newStructureGroup.group.units[0];
@@ -239,7 +265,8 @@ export function IntraUnitBondCylinderMeshVisual(materialId: number): UnitsVisual
                 newProps.stubCap !== currentProps.stubCap ||
                 !arrayEqual(newProps.includeTypes, currentProps.includeTypes) ||
                 !arrayEqual(newProps.excludeTypes, currentProps.excludeTypes) ||
-                newProps.adjustCylinderLength !== currentProps.adjustCylinderLength
+                newProps.adjustCylinderLength !== currentProps.adjustCylinderLength ||
+                newProps.aromaticBonds !== currentProps.aromaticBonds
             );
 
             const newUnit = newStructureGroup.group.units[0];

+ 40 - 14
src/mol-repr/structure/visual/bond-intra-unit-line.ts

@@ -18,6 +18,7 @@ import { BondIterator, BondLineParams, getIntraBondLoci, eachIntraBond, makeIntr
 import { Sphere3D } from '../../../mol-math/geometry';
 import { Lines } from '../../../mol-geo/geometry/lines/lines';
 import { IntAdjacencyGraph } from '../../../mol-math/graph';
+import { arrayIntersectionSize } from '../../../mol-util/array';
 
 // avoiding namespace lookup improved performance in Chrome (Aug 2020)
 const isBondType = BondType.is;
@@ -35,31 +36,44 @@ function createIntraUnitBondLines(ctx: VisualContext, unit: Unit, structure: Str
     const bonds = unit.bonds;
     const { edgeCount, a, b, edgeProps, offset } = bonds;
     const { order: _order, flags: _flags } = edgeProps;
-    const { sizeFactor } = props;
+    const { sizeFactor, aromaticBonds } = props;
 
     if (!edgeCount) return Lines.createEmpty(lines);
 
     const vRef = Vec3();
     const pos = unit.conformation.invariantPosition;
 
-    const builderProps = {
+    const { elementRingIndices, elementAromaticRingIndices } = unit.rings;
+
+    const builderProps: LinkBuilderProps = {
         linkCount: edgeCount * 2,
         referencePosition: (edgeIndex: number) => {
             let aI = a[edgeIndex], bI = b[edgeIndex];
 
             if (aI > bI) [aI, bI] = [bI, aI];
             if (offset[aI + 1] - offset[aI] === 1) [aI, bI] = [bI, aI];
-            // TODO prefer reference atoms within rings
+
+            const aR = elementRingIndices.get(aI);
+            let maxSize = 0;
 
             for (let i = offset[aI], il = offset[aI + 1]; i < il; ++i) {
                 const _bI = b[i];
-                if (_bI !== bI && _bI !== aI) return pos(elements[_bI], vRef);
-            }
-            for (let i = offset[bI], il = offset[bI + 1]; i < il; ++i) {
-                const _aI = a[i];
-                if (_aI !== aI && _aI !== bI) return pos(elements[_aI], vRef);
+                if (_bI !== bI && _bI !== aI) {
+                    if (aR) {
+                        const _bR = elementRingIndices.get(_bI);
+                        if (!_bR) continue;
+
+                        const size = arrayIntersectionSize(aR, _bR);
+                        if (size > maxSize) {
+                            maxSize = size;
+                            pos(elements[_bI], vRef);
+                        }
+                    } else {
+                        return pos(elements[_bI], vRef);
+                    }
+                }
             }
-            return null;
+            return maxSize > 0 ? vRef : null;
         },
         position: (posA: Vec3, posB: Vec3, edgeIndex: number) => {
             pos(elements[a[edgeIndex]], posA);
@@ -71,13 +85,24 @@ function createIntraUnitBondLines(ctx: VisualContext, unit: Unit, structure: Str
             if (isBondType(f, BondType.Flag.MetallicCoordination) || isBondType(f, BondType.Flag.HydrogenBond)) {
                 // show metall coordinations and hydrogen bonds with dashed cylinders
                 return LinkStyle.Dashed;
-            } else if (o === 2) {
-                return LinkStyle.Double;
             } else if (o === 3) {
                 return LinkStyle.Triple;
-            } else {
-                return LinkStyle.Solid;
+            } else if (aromaticBonds) {
+                const aI = a[edgeIndex], bI = b[edgeIndex];
+                const aR = elementAromaticRingIndices.get(aI);
+                const bR = elementAromaticRingIndices.get(bI);
+                const arCount = (aR && bR) ? arrayIntersectionSize(aR, bR) : 0;
+
+                if (arCount || isBondType(f, BondType.Flag.Aromatic)) {
+                    if (arCount === 2) {
+                        return LinkStyle.MirroredAromatic;
+                    } else {
+                        return LinkStyle.Aromatic;
+                    }
+                }
             }
+
+            return o === 2 ? LinkStyle.Double : LinkStyle.Solid;
         },
         radius: (edgeIndex: number) => {
             location.element = elements[a[edgeIndex]];
@@ -119,7 +144,8 @@ export function IntraUnitBondLineVisual(materialId: number): UnitsVisual<IntraUn
                 newProps.dashCount !== currentProps.dashCount ||
                 newProps.ignoreHydrogens !== currentProps.ignoreHydrogens ||
                 !arrayEqual(newProps.includeTypes, currentProps.includeTypes) ||
-                !arrayEqual(newProps.excludeTypes, currentProps.excludeTypes)
+                !arrayEqual(newProps.excludeTypes, currentProps.excludeTypes) ||
+                newProps.aromaticBonds !== currentProps.aromaticBonds
             );
 
             const newUnit = newStructureGroup.group.units[0];

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

@@ -20,6 +20,7 @@ export const BondParams = {
     includeTypes: PD.MultiSelect(ObjectKeys(BondType.Names), PD.objectToOptions(BondType.Names)),
     excludeTypes: PD.MultiSelect([] as BondType.Names[], PD.objectToOptions(BondType.Names)),
     ignoreHydrogens: PD.Boolean(false),
+    aromaticBonds: PD.Boolean(false, { description: 'Display aromatic bonds with dashes' }),
 };
 export const DefaultBondProps = PD.getDefaultValues(BondParams);
 export type BondProps = typeof DefaultBondProps

+ 103 - 26
src/mol-repr/structure/visual/util/link.ts

@@ -18,7 +18,7 @@ import { Cylinders } from '../../../../mol-geo/geometry/cylinders/cylinders';
 import { CylindersBuilder } from '../../../../mol-geo/geometry/cylinders/cylinders-builder';
 
 export const LinkCylinderParams = {
-    linkScale: PD.Numeric(0.4, { min: 0, max: 1, step: 0.1 }),
+    linkScale: PD.Numeric(0.45, { min: 0, max: 1, step: 0.01 }),
     linkSpacing: PD.Numeric(1, { min: 0, max: 2, step: 0.01 }),
     linkCap: PD.Boolean(false),
     dashCount: PD.Numeric(4, { min: 2, max: 10, step: 2 }),
@@ -84,7 +84,9 @@ export const enum LinkStyle {
     Dashed = 1,
     Double = 2,
     Triple = 3,
-    Disk = 4
+    Disk = 4,
+    Aromatic = 5,
+    MirroredAromatic = 6,
 }
 
 // avoiding namespace lookup improved performance in Chrome (Aug 2020)
@@ -133,6 +135,8 @@ export function createLinkCylinderMesh(ctx: VisualContext, linkBuilder: LinkBuil
         const [topCap, bottomCap] = (v3dot(tmpV12, up) > 0) ? [linkStub, linkCap] : [linkCap, linkStub];
         builderState.currentGroup = edgeIndex;
 
+        const aromaticOffsetFactor = 4.5;
+
         if (linkStyle === LinkStyle.Solid) {
             cylinderProps.radiusTop = cylinderProps.radiusBottom = linkRadius;
             cylinderProps.topCap = topCap;
@@ -144,20 +148,41 @@ export function createLinkCylinderMesh(ctx: VisualContext, linkBuilder: LinkBuil
             cylinderProps.topCap = cylinderProps.bottomCap = dashCap;
 
             addFixedCountDashedCylinder(builderState, va, vb, 0.5, segmentCount, cylinderProps);
-        } else if (linkStyle === LinkStyle.Double || linkStyle === LinkStyle.Triple) {
-            const order = linkStyle === LinkStyle.Double ? 2 : 3;
+        } else if (linkStyle === LinkStyle.Double || linkStyle === LinkStyle.Triple || linkStyle === LinkStyle.Aromatic || linkStyle === LinkStyle.MirroredAromatic) {
+            const order = linkStyle === LinkStyle.Double ? 2 :
+                linkStyle === LinkStyle.Triple ? 3 : 1.5;
             const multiRadius = linkRadius * (linkScale / (0.5 * order));
             const absOffset = (linkRadius - multiRadius) * linkSpacing;
 
             calculateShiftDir(vShift, va, vb, referencePosition ? referencePosition(edgeIndex) : null);
-            v3setMagnitude(vShift, vShift, absOffset);
 
-            cylinderProps.radiusTop = cylinderProps.radiusBottom = multiRadius;
             cylinderProps.topCap = topCap;
             cylinderProps.bottomCap = bottomCap;
 
-            if (order === 3) addCylinder(builderState, va, vb, 0.5, cylinderProps);
-            addDoubleCylinder(builderState, va, vb, 0.5, vShift, cylinderProps);
+            if (linkStyle === LinkStyle.Aromatic || linkStyle === LinkStyle.MirroredAromatic) {
+                cylinderProps.radiusTop = cylinderProps.radiusBottom = linkRadius;
+                addCylinder(builderState, va, vb, 0.5, cylinderProps);
+
+                cylinderProps.radiusTop = cylinderProps.radiusBottom = linkRadius * linkScale;
+                cylinderProps.topCap = cylinderProps.bottomCap = dashCap;
+                v3setMagnitude(vShift, vShift, absOffset * aromaticOffsetFactor);
+                v3sub(va, va, vShift);
+                v3sub(vb, vb, vShift);
+                addFixedCountDashedCylinder(builderState, va, vb, 0.5, 3, cylinderProps);
+
+                if (linkStyle === LinkStyle.MirroredAromatic) {
+                    v3setMagnitude(vShift, vShift, absOffset * aromaticOffsetFactor * 2);
+                    v3add(va, va, vShift);
+                    v3add(vb, vb, vShift);
+                    addFixedCountDashedCylinder(builderState, va, vb, 0.5, 3, cylinderProps);
+                }
+            } else {
+                v3setMagnitude(vShift, vShift, absOffset);
+
+                cylinderProps.radiusTop = cylinderProps.radiusBottom = multiRadius;
+                if (order === 3) addCylinder(builderState, va, vb, 0.5, cylinderProps);
+                addDoubleCylinder(builderState, va, vb, 0.5, vShift, cylinderProps);
+            }
         } else if (linkStyle === LinkStyle.Disk) {
             v3scale(tmpV12, tmpV12, 0.475);
             v3add(va, va, tmpV12);
@@ -190,12 +215,17 @@ export function createLinkCylinderImpostors(ctx: VisualContext, linkBuilder: Lin
 
     const va = Vec3();
     const vb = Vec3();
+    const vm = Vec3();
     const vShift = Vec3();
 
     // automatically adjust length for evenly spaced dashed cylinders
     const segmentCount = dashCount % 2 === 1 ? dashCount : dashCount + 1;
     const lengthScale = 0.5 - (0.5 / 2 / segmentCount);
 
+    const aromaticSegmentCount = 3;
+    const aromaticLengthScale = 0.5 - (0.5 / 2 / aromaticSegmentCount);
+    const aromaticOffsetFactor = 4.5;
+
     for (let edgeIndex = 0, _eI = linkCount; edgeIndex < _eI; ++edgeIndex) {
         if (ignore && ignore(edgeIndex)) continue;
 
@@ -206,24 +236,45 @@ export function createLinkCylinderImpostors(ctx: VisualContext, linkBuilder: Lin
         const linkStub = stubCap && (stub ? stub(edgeIndex) : false);
 
         if (linkStyle === LinkStyle.Solid) {
-            v3scale(vb, v3add(vb, va, vb), 0.5);
-            builder.add(va[0], va[1], va[2], vb[0], vb[1], vb[2], 1, linkCap, linkStub, edgeIndex);
+            v3scale(vm, v3add(vm, va, vb), 0.5);
+            builder.add(va[0], va[1], va[2], vm[0], vm[1], vm[2], 1, linkCap, linkStub, edgeIndex);
         } else if (linkStyle === LinkStyle.Dashed) {
             v3scale(tmpV12, v3sub(tmpV12, vb, va), lengthScale);
             v3sub(vb, vb, tmpV12);
             builder.addFixedCountDashes(va, vb, segmentCount, dashScale, dashCap, dashCap, edgeIndex);
-        } else if (linkStyle === LinkStyle.Double || linkStyle === LinkStyle.Triple) {
-            v3scale(vb, v3add(vb, va, vb), 0.5);
-            const order = linkStyle === LinkStyle.Double ? 2 : 3;
+        } else if (linkStyle === LinkStyle.Double || linkStyle === LinkStyle.Triple || linkStyle === LinkStyle.Aromatic || linkStyle === LinkStyle.MirroredAromatic) {
+            const order = linkStyle === LinkStyle.Double ? 2 :
+                linkStyle === LinkStyle.Triple ? 3 : 1.5;
             const multiScale = linkScale / (0.5 * order);
             const absOffset = (linkRadius - multiScale * linkRadius) * linkSpacing;
 
+            v3scale(vm, v3add(vm, va, vb), 0.5);
             calculateShiftDir(vShift, va, vb, referencePosition ? referencePosition(edgeIndex) : null);
-            v3setMagnitude(vShift, vShift, absOffset);
 
-            if (order === 3) builder.add(va[0], va[1], va[2], vb[0], vb[1], vb[2], multiScale, linkCap, false, edgeIndex);
-            builder.add(va[0] + vShift[0], va[1] + vShift[1], va[2] + vShift[2], vb[0] + vShift[0], vb[1] + vShift[1], vb[2] + vShift[2], multiScale, linkCap, linkStub, edgeIndex);
-            builder.add(va[0] - vShift[0], va[1] - vShift[1], va[2] - vShift[2], vb[0] - vShift[0], vb[1] - vShift[1], vb[2] - vShift[2], multiScale, linkCap, linkStub, edgeIndex);
+            if (linkStyle === LinkStyle.Aromatic || linkStyle === LinkStyle.MirroredAromatic) {
+                builder.add(va[0], va[1], va[2], vm[0], vm[1], vm[2], 1, linkCap, linkStub, edgeIndex);
+
+                v3scale(tmpV12, v3sub(tmpV12, vb, va), aromaticLengthScale);
+                v3sub(vb, vb, tmpV12);
+
+                v3setMagnitude(vShift, vShift, absOffset * aromaticOffsetFactor);
+                v3sub(va, va, vShift);
+                v3sub(vb, vb, vShift);
+                builder.addFixedCountDashes(va, vb, aromaticSegmentCount, linkScale, dashCap, dashCap, edgeIndex);
+
+                if (linkStyle === LinkStyle.MirroredAromatic) {
+                    v3setMagnitude(vShift, vShift, absOffset * aromaticOffsetFactor * 2);
+                    v3add(va, va, vShift);
+                    v3add(vb, vb, vShift);
+                    builder.addFixedCountDashes(va, vb, aromaticSegmentCount, linkScale, dashCap, dashCap, edgeIndex);
+                }
+            } else {
+                v3setMagnitude(vShift, vShift, absOffset);
+
+                if (order === 3) builder.add(va[0], va[1], va[2], vm[0], vm[1], vm[2], multiScale, linkCap, false, edgeIndex);
+                builder.add(va[0] + vShift[0], va[1] + vShift[1], va[2] + vShift[2], vm[0] + vShift[0], vm[1] + vShift[1], vm[2] + vShift[2], multiScale, linkCap, linkStub, edgeIndex);
+                builder.add(va[0] - vShift[0], va[1] - vShift[1], va[2] - vShift[2], vm[0] - vShift[0], vm[1] - vShift[1], vm[2] - vShift[2], multiScale, linkCap, linkStub, edgeIndex);
+            }
         } else if (linkStyle === LinkStyle.Disk) {
             v3scale(tmpV12, v3sub(tmpV12, vb, va), 0.475);
             v3add(va, va, tmpV12);
@@ -252,12 +303,17 @@ export function createLinkLines(ctx: VisualContext, linkBuilder: LinkBuilderProp
 
     const va = Vec3();
     const vb = Vec3();
+    const vm = Vec3();
     const vShift = Vec3();
 
     // automatically adjust length for evenly spaced dashed lines
     const segmentCount = dashCount % 2 === 1 ? dashCount : dashCount + 1;
     const lengthScale = 0.5 - (0.5 / 2 / segmentCount);
 
+    const aromaticSegmentCount = 3;
+    const aromaticLengthScale = 0.5 - (0.5 / 2 / aromaticSegmentCount);
+    const aromaticOffsetFactor = 4.5;
+
     for (let edgeIndex = 0, _eI = linkCount; edgeIndex < _eI; ++edgeIndex) {
         if (ignore && ignore(edgeIndex)) continue;
 
@@ -266,24 +322,45 @@ export function createLinkLines(ctx: VisualContext, linkBuilder: LinkBuilderProp
         const linkStyle = style ? style(edgeIndex) : LinkStyle.Solid;
 
         if (linkStyle === LinkStyle.Solid) {
-            v3scale(vb, v3add(vb, va, vb), 0.5);
-            builder.add(va[0], va[1], va[2], vb[0], vb[1], vb[2], edgeIndex);
+            v3scale(vm, v3add(vm, va, vb), 0.5);
+            builder.add(va[0], va[1], va[2], vm[0], vm[1], vm[2], edgeIndex);
         } else if (linkStyle === LinkStyle.Dashed) {
             v3scale(tmpV12, v3sub(tmpV12, vb, va), lengthScale);
             v3sub(vb, vb, tmpV12);
             builder.addFixedCountDashes(va, vb, segmentCount, edgeIndex);
-        } else if (linkStyle === LinkStyle.Double || linkStyle === LinkStyle.Triple) {
-            v3scale(vb, v3add(vb, va, vb), 0.5);
-            const order = linkStyle === LinkStyle.Double ? 2 : 3;
+        } else if (linkStyle === LinkStyle.Double || linkStyle === LinkStyle.Triple || linkStyle === LinkStyle.Aromatic || linkStyle === LinkStyle.MirroredAromatic) {
+            const order = linkStyle === LinkStyle.Double ? 2 :
+                linkStyle === LinkStyle.Triple ? 3 : 1.5;
             const multiRadius = 1 * (linkScale / (0.5 * order));
             const absOffset = (1 - multiRadius) * linkSpacing;
 
+            v3scale(vm, v3add(vm, va, vb), 0.5);
             calculateShiftDir(vShift, va, vb, referencePosition ? referencePosition(edgeIndex) : null);
-            v3setMagnitude(vShift, vShift, absOffset);
 
-            if (order === 3) builder.add(va[0], va[1], va[2], vb[0], vb[1], vb[2], edgeIndex);
-            builder.add(va[0] + vShift[0], va[1] + vShift[1], va[2] + vShift[2], vb[0] + vShift[0], vb[1] + vShift[1], vb[2] + vShift[2], edgeIndex);
-            builder.add(va[0] - vShift[0], va[1] - vShift[1], va[2] - vShift[2], vb[0] - vShift[0], vb[1] - vShift[1], vb[2] - vShift[2], edgeIndex);
+            if (linkStyle === LinkStyle.Aromatic || linkStyle === LinkStyle.MirroredAromatic) {
+                builder.add(va[0], va[1], va[2], vm[0], vm[1], vm[2], edgeIndex);
+
+                v3scale(tmpV12, v3sub(tmpV12, vb, va), aromaticLengthScale);
+                v3sub(vb, vb, tmpV12);
+
+                v3setMagnitude(vShift, vShift, absOffset * aromaticOffsetFactor);
+                v3sub(va, va, vShift);
+                v3sub(vb, vb, vShift);
+                builder.addFixedCountDashes(va, vb, aromaticSegmentCount, edgeIndex);
+
+                if (linkStyle === LinkStyle.MirroredAromatic) {
+                    v3setMagnitude(vShift, vShift, absOffset * aromaticOffsetFactor * 2);
+                    v3add(va, va, vShift);
+                    v3add(vb, vb, vShift);
+                    builder.addFixedCountDashes(va, vb, aromaticSegmentCount, edgeIndex);
+                }
+            } else {
+                v3setMagnitude(vShift, vShift, absOffset);
+
+                if (order === 3) builder.add(va[0], va[1], va[2], vm[0], vm[1], vm[2], edgeIndex);
+                builder.add(va[0] + vShift[0], va[1] + vShift[1], va[2] + vShift[2], vm[0] + vShift[0], vm[1] + vShift[1], vm[2] + vShift[2], edgeIndex);
+                builder.add(va[0] - vShift[0], va[1] - vShift[1], va[2] - vShift[2], vm[0] - vShift[0], vm[1] - vShift[1], vm[2] - vShift[2], edgeIndex);
+            }
         } else if (linkStyle === LinkStyle.Disk) {
             v3scale(tmpV12, v3sub(tmpV12, vb, va), 0.475);
             v3add(va, va, tmpV12);

+ 24 - 1
src/mol-util/array.ts

@@ -1,5 +1,5 @@
 /**
- * Copyright (c) 2018-2019 mol* contributors, licensed under MIT, See LICENSE file for more info.
+ * Copyright (c) 2018-2021 mol* contributors, licensed under MIT, See LICENSE file for more info.
  *
  * @author Alexander Rose <alexander.rose@weirdbyte.de>
  */
@@ -105,6 +105,29 @@ export function arraySetRemove<T>(xs: T[], x: T) {
     return true;
 }
 
+/**
+ * Caution, O(n^2) complexity. Only use for small input sizes.
+ * For larger inputs consider using `SortedArray`.
+ */
+export function arrayAreIntersecting<T>(xs: T[], ys: T[]) {
+    for (let i = 0, il = xs.length; i < il; ++i) {
+        if (ys.includes(xs[i])) return true;
+    }
+    return false;
+}
+
+/**
+ * Caution, O(n^2) complexity. Only use for small input sizes.
+ * For larger inputs consider using `SortedArray`.
+ */
+export function arrayIntersectionSize<T>(xs: T[], ys: T[]) {
+    let count = 0;
+    for (let i = 0, il = xs.length; i < il; ++i) {
+        if (ys.includes(xs[i])) count += 1;
+    }
+    return count;
+}
+
 export function arrayEqual<T>(xs?: ArrayLike<T>, ys?: ArrayLike<T>) {
     if (!xs || xs.length === 0) return !ys || ys.length === 0;
     if (!ys) return false;