Browse Source

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

Alexander Rose 4 years ago
parent
commit
5858a6eb19

+ 1 - 1
package-lock.json

@@ -1,6 +1,6 @@
 {
   "name": "molstar",
-  "version": "1.1.11",
+  "version": "1.1.14",
   "lockfileVersion": 1,
   "requires": true,
   "dependencies": {

+ 1 - 1
package.json

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

+ 11 - 8
src/mol-math/graph/int-adjacency-graph.ts

@@ -17,13 +17,14 @@ import { AssignableArrayLike } from '../../mol-util/type-helpers';
  *
  * Edge properties are indexed same as in the arrays a and b.
  */
-export interface IntAdjacencyGraph<VertexIndex extends number, EdgeProps extends IntAdjacencyGraph.EdgePropsBase> {
+export interface IntAdjacencyGraph<VertexIndex extends number, EdgeProps extends IntAdjacencyGraph.EdgePropsBase, Props = any> {
     readonly offset: ArrayLike<number>,
     readonly a: ArrayLike<VertexIndex>,
     readonly b: ArrayLike<VertexIndex>,
     readonly vertexCount: number,
     readonly edgeCount: number,
-    readonly edgeProps: Readonly<EdgeProps>
+    readonly edgeProps: Readonly<EdgeProps>,
+    readonly props?: Props
 
     /**
      * Get the edge index between i-th and j-th vertex.
@@ -49,6 +50,8 @@ export namespace IntAdjacencyGraph {
     export type EdgePropsBase = { [name: string]: ArrayLike<any> }
 
     export function areEqual<I extends number, P extends IntAdjacencyGraph.EdgePropsBase>(a: IntAdjacencyGraph<I, P>, b: IntAdjacencyGraph<I, P>) {
+        if (a === b) return true;
+
         if (a.vertexCount !== b.vertexCount || a.edgeCount !== b.edgeCount) return false;
 
         const { a: aa, b: ab, offset: ao } = a;
@@ -78,7 +81,7 @@ export namespace IntAdjacencyGraph {
         return true;
     }
 
-    class IntGraphImpl<VertexIndex extends number, EdgeProps extends IntAdjacencyGraph.EdgePropsBase> implements IntAdjacencyGraph<VertexIndex, EdgeProps> {
+    class IntGraphImpl<VertexIndex extends number, EdgeProps extends IntAdjacencyGraph.EdgePropsBase, Props> implements IntAdjacencyGraph<VertexIndex, EdgeProps, Props> {
         readonly vertexCount: number;
         readonly edgeProps: EdgeProps;
 
@@ -106,14 +109,14 @@ export namespace IntAdjacencyGraph {
             return this.offset[i + 1] - this.offset[i];
         }
 
-        constructor(public offset: ArrayLike<number>, public a: ArrayLike<VertexIndex>, public b: ArrayLike<VertexIndex>, public edgeCount: number, edgeProps?: EdgeProps) {
+        constructor(public offset: ArrayLike<number>, public a: ArrayLike<VertexIndex>, public b: ArrayLike<VertexIndex>, public edgeCount: number, edgeProps?: EdgeProps, public props?: Props) {
             this.vertexCount = offset.length - 1;
             this.edgeProps = (edgeProps || {}) as EdgeProps;
         }
     }
 
-    export function create<VertexIndex extends number, EdgeProps extends IntAdjacencyGraph.EdgePropsBase>(offset: ArrayLike<number>, a: ArrayLike<VertexIndex>, b: ArrayLike<VertexIndex>, edgeCount: number, edgeProps?: EdgeProps): IntAdjacencyGraph<VertexIndex, EdgeProps> {
-        return new IntGraphImpl(offset, a, b, edgeCount, edgeProps) as IntAdjacencyGraph<VertexIndex, EdgeProps>;
+    export function create<VertexIndex extends number, EdgeProps extends IntAdjacencyGraph.EdgePropsBase, Props>(offset: ArrayLike<number>, a: ArrayLike<VertexIndex>, b: ArrayLike<VertexIndex>, edgeCount: number, edgeProps?: EdgeProps, props?: Props): IntAdjacencyGraph<VertexIndex, EdgeProps, Props> {
+        return new IntGraphImpl(offset, a, b, edgeCount, edgeProps, props) as IntAdjacencyGraph<VertexIndex, EdgeProps, Props>;
     }
 
     export class EdgeBuilder<VertexIndex extends number> {
@@ -129,8 +132,8 @@ export namespace IntAdjacencyGraph {
         a: AssignableArrayLike<VertexIndex>;
         b: AssignableArrayLike<VertexIndex>;
 
-        createGraph<EdgeProps extends IntAdjacencyGraph.EdgePropsBase>(edgeProps: EdgeProps) {
-            return create<VertexIndex, EdgeProps>(this.offsets, this.a, this.b, this.edgeCount, edgeProps);
+        createGraph<EdgeProps extends IntAdjacencyGraph.EdgePropsBase, Props>(edgeProps: EdgeProps, props?: Props) {
+            return create<VertexIndex, EdgeProps, Props>(this.offsets, this.a, this.b, this.edgeCount, edgeProps, props);
         }
 
         /**

+ 2 - 4
src/mol-model-formats/structure/basic/atomic.ts

@@ -62,8 +62,7 @@ function createHierarchyData(atom_site: AtomSite, sourceIndex: Column<number>, o
         label_alt_id: atom_site.label_alt_id,
         label_comp_id: atom_site.label_comp_id,
         auth_comp_id: atom_site.auth_comp_id,
-        pdbx_formal_charge: atom_site.pdbx_formal_charge,
-        sourceIndex
+        pdbx_formal_charge: atom_site.pdbx_formal_charge
     });
 
     const residues = Table.view(atom_site, ResiduesSchema, offsets.residues);
@@ -94,7 +93,7 @@ function createHierarchyData(atom_site: AtomSite, sourceIndex: Column<number>, o
     substUndefinedColumn(residues, 'label_seq_id', 'auth_seq_id');
     substUndefinedColumn(chains, 'label_asym_id', 'auth_asym_id');
 
-    return { atoms, residues, chains };
+    return { atoms, residues, chains, atomSourceIndex: sourceIndex };
 }
 
 function getConformation(atom_site: AtomSite): AtomicConformation {
@@ -111,7 +110,6 @@ function getConformation(atom_site: AtomSite): AtomicConformation {
 }
 
 function isHierarchyDataEqual(a: AtomicData, b: AtomicData) {
-    // TODO need to cast because of how TS handles type resolution for interfaces https://github.com/Microsoft/TypeScript/issues/15300
     return Table.areEqual(a.chains, b.chains)
         && Table.areEqual(a.residues, b.residues)
         && Table.areEqual(a.atoms, b.atoms);

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

@@ -26,7 +26,7 @@ export function trajectoryFromPDB(pdb: PdbFile): Task<Trajectory> {
             //      would need to do model splitting again
             if (models.frameCount === 1) {
                 const first = models.representative;
-                const srcIndex = first.atomicHierarchy.atoms.sourceIndex;
+                const srcIndex = first.atomicHierarchy.atomSourceIndex;
                 const isIdentity = Column.isIdentity(srcIndex);
                 const srcIndexArray = isIdentity ? void 0 : srcIndex.toArray({ array: Int32Array });
 

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

@@ -92,7 +92,7 @@ export namespace Model {
         const trajectory: Model[] = [];
         const { frames } = coordinates;
 
-        const srcIndex = model.atomicHierarchy.atoms.sourceIndex;
+        const srcIndex = model.atomicHierarchy.atomSourceIndex;
         const isIdentity = Column.isIdentity(srcIndex);
         const srcIndexArray = isIdentity ? void 0 : srcIndex.toArray({ array: Int32Array });
 

+ 5 - 6
src/mol-model/structure/model/properties/atomic/hierarchy.ts

@@ -50,12 +50,6 @@ export const AtomsSchema = {
      */
     pdbx_formal_charge: mmCIF.atom_site.pdbx_formal_charge,
 
-    /**
-     * The index of this atom in the input data.
-     * Required because of sorting of atoms.
-     */
-    sourceIndex: Column.Schema.int
-
     // id, occupancy and B_iso_or_equiv are part of conformation
 };
 
@@ -108,6 +102,11 @@ export type Chains = Table<ChainsSchema>
 
 export interface AtomicData {
     atoms: Atoms,
+    /**
+     * The index of this atom in the input data.
+     * Required because of sorting of atoms.
+     */
+    atomSourceIndex: Column<number>,
     residues: Residues,
     chains: Chains
 }

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

@@ -529,7 +529,7 @@ export namespace Loci {
 
     function sourceIndex(unit: Unit, element: ElementIndex) {
         return Unit.isAtomic(unit)
-            ? unit.model.atomicHierarchy.atoms.sourceIndex.value(element)
+            ? unit.model.atomicHierarchy.atomSourceIndex.value(element)
             // TODO: when implemented, this should map to the source index.
             : element;
     }

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

@@ -42,7 +42,7 @@ const atom = {
     occupancy: p(l => !Unit.isAtomic(l.unit) ?  notAtomic() : l.unit.model.atomicConformation.occupancy.value(l.element)),
     B_iso_or_equiv: p(l => !Unit.isAtomic(l.unit) ?  notAtomic() : l.unit.model.atomicConformation.B_iso_or_equiv.value(l.element)),
     sourceIndex: p(l => Unit.isAtomic(l.unit)
-        ? l.unit.model.atomicHierarchy.atoms.sourceIndex.value(l.element)
+        ? l.unit.model.atomicHierarchy.atomSourceIndex.value(l.element)
         // TODO: when implemented, this should map to the source index.
         : l.element),
 

+ 44 - 2
src/mol-model/structure/structure/unit.ts

@@ -22,6 +22,8 @@ import { PrincipalAxes } from '../../../mol-math/linear-algebra/matrix/principal
 import { getPrincipalAxes } from './util/principal-axes';
 import { Boundary, getBoundary, tryAdjustBoundary } from '../../../mol-math/geometry/boundary';
 import { Mat4 } 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';
 
 /**
  * A building block of a structure that corresponds to an atomic or
@@ -209,7 +211,7 @@ namespace Unit {
                 const { x, y, z } = this.model.atomicConformation;
                 boundary = tryAdjustBoundary({ x, y, z, indices: this.elements }, boundary);
             }
-            const props = { ...this.props, boundary, lookup3d: undefined, principalAxes: undefined };
+            const props = { ...this.props, bonds: tryRemapBonds(this, this.props.bonds, model), boundary, lookup3d: undefined, principalAxes: undefined };
             const conformation = this.model.atomicConformation !== model.atomicConformation
                 ? SymmetryOperator.createMapping(this.conformation.operator, model.atomicConformation)
                 : this.conformation;
@@ -238,7 +240,14 @@ namespace Unit {
 
         get bonds() {
             if (this.props.bonds) return this.props.bonds;
-            this.props.bonds = computeIntraUnitBonds(this);
+
+            const cache = ElementSetIntraBondCache.get(this.model);
+            let bonds = cache.get(this.elements);
+            if (!bonds) {
+                bonds = computeIntraUnitBonds(this);
+                cache.set(this.elements, bonds);
+            }
+            this.props.bonds = bonds;
             return this.props.bonds;
         }
 
@@ -455,6 +464,39 @@ namespace Unit {
 
         return true;
     }
+
+    function tryRemapBonds(a: Atomic, old: IntraUnitBonds | undefined, model: Model) {
+        // TODO: should include additional checks?
+
+        if (!old) return void 0;
+        if (a.model.atomicConformation.id === model.atomicConformation.id) return old;
+
+        const oldIndex = IndexPairBonds.Provider.get(a.model);
+        if (oldIndex) {
+            const newIndex = IndexPairBonds.Provider.get(model);
+            // TODO: check the actual indices instead of just reference equality?
+            if (!newIndex || oldIndex === newIndex) return old;
+            return void 0;
+        }
+
+        if (old.props?.canRemap) {
+            return old;
+        }
+        return isSameConformation(a, model) ? old : void 0;
+    }
+
+    function isSameConformation(a: Atomic, model: Model) {
+        const xs = a.elements;
+        const { x: xa, y: ya, z: za } = a.conformation.coordinates;
+        const { x: xb, y: yb, z: zb } = model.atomicConformation;
+
+        for (let i = 0, _i = xs.length; i < _i; i++) {
+            const u = xs[i];
+            if (xa[u] !== xb[u] || ya[u] !== yb[u] || za[u] !== zb[u]) return false;
+        }
+
+        return true;
+    }
 }
 
 export default Unit;

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

@@ -12,7 +12,7 @@ import StructureElement from '../../element';
 import { Bond } from '../bonds';
 import { InterUnitGraph } from '../../../../../mol-math/graph/inter-unit-graph';
 
-type IntraUnitBonds = IntAdjacencyGraph<StructureElement.UnitIndex, { readonly order: ArrayLike<number>, readonly flags: ArrayLike<BondType.Flag> }>
+type IntraUnitBonds = IntAdjacencyGraph<StructureElement.UnitIndex, { readonly order: ArrayLike<number>, readonly flags: ArrayLike<BondType.Flag> }, { readonly canRemap?: boolean }>
 
 namespace IntraUnitBonds {
     export const Empty: IntraUnitBonds = IntAdjacencyGraph.create([], [], [], 0, { flags: [], order: [] });

+ 45 - 0
src/mol-model/structure/structure/unit/bonds/element-set-intra-bond-cache.ts

@@ -0,0 +1,45 @@
+/**
+ * Copyright (c) 2020 mol* contributors, licensed under MIT, See LICENSE file for more info.
+ *
+ * @author David Sehnal <david.sehnal@gmail.com>
+ */
+
+import StructureElement from '../../element';
+import { IntraUnitBonds } from './data';
+import { SortedArray } from '../../../../../mol-data/int';
+import { Model } from '../../../model';
+
+export class ElementSetIntraBondCache {
+    private data = new Map<number, [StructureElement.Set, IntraUnitBonds][]>();
+
+    get(xs: StructureElement.Set): IntraUnitBonds | undefined {
+        const hash = SortedArray.hashCode(xs);
+        if (!this.data.has(hash)) return void 0;
+        for (const [s, b] of this.data.get(hash)!) {
+            if (SortedArray.areEqual(xs, s)) return b;
+        }
+    }
+
+    set(xs: StructureElement.Set, bonds: IntraUnitBonds) {
+        const hash = SortedArray.hashCode(xs);
+        if (this.data.has(hash)) {
+            const es = this.data.get(hash)!;
+            for (const e of es) {
+                if (SortedArray.areEqual(xs, e[0])) {
+                    e[1] = bonds;
+                    return;
+                }
+            }
+            es.push([xs, bonds]);
+        } else {
+            this.data.set(hash, [[xs, bonds]]);
+        }
+    }
+
+    static get(model: Model): ElementSetIntraBondCache {
+        if (!model._dynamicPropertyData.ElementSetIntraBondCache) {
+            model._dynamicPropertyData.ElementSetIntraBondCache = new ElementSetIntraBondCache();
+        }
+        return model._dynamicPropertyData.ElementSetIntraBondCache;
+    }
+}

+ 60 - 28
src/mol-model/structure/structure/unit/bonds/intra-compute.ts

@@ -20,7 +20,7 @@ import { Vec3 } from '../../../../../mol-math/linear-algebra';
 import { ElementIndex } from '../../../model/indexing';
 import { equalEps } from '../../../../../mol-math/linear-algebra/3d/common';
 
-function getGraph(atomA: StructureElement.UnitIndex[], atomB: StructureElement.UnitIndex[], _order: number[], _flags: number[], atomCount: number): IntraUnitBonds {
+function getGraph(atomA: StructureElement.UnitIndex[], atomB: StructureElement.UnitIndex[], _order: number[], _flags: number[], atomCount: number, canRemap: boolean): IntraUnitBonds {
     const builder = new IntAdjacencyGraph.EdgeBuilder(atomCount, atomA, atomB);
     const flags = new Uint16Array(builder.slotCount);
     const order = new Int8Array(builder.slotCount);
@@ -30,7 +30,7 @@ function getGraph(atomA: StructureElement.UnitIndex[], atomB: StructureElement.U
         builder.assignProperty(order, _order[i]);
     }
 
-    return builder.createGraph({ flags, order });
+    return builder.createGraph({ flags, order }, { canRemap });
 }
 
 const tmpDistVecA = Vec3();
@@ -43,7 +43,44 @@ function getDistance(unit: Unit.Atomic, indexA: ElementIndex, indexB: ElementInd
 
 const __structConnAdded = new Set<StructureElement.UnitIndex>();
 
-function _computeBonds(unit: Unit.Atomic, props: BondComputationProps): IntraUnitBonds {
+function findIndexPairBonds(unit: Unit.Atomic) {
+    const indexPairs = IndexPairBonds.Provider.get(unit.model)!;
+    const { elements: atoms } = unit;
+    const { type_symbol } = unit.model.atomicHierarchy.atoms;
+    const atomCount = unit.elements.length;
+    const { edgeProps } = indexPairs;
+
+    const atomA: StructureElement.UnitIndex[] = [];
+    const atomB: StructureElement.UnitIndex[] = [];
+    const flags: number[] = [];
+    const order: number[] = [];
+
+    for (let _aI = 0 as StructureElement.UnitIndex; _aI < atomCount; _aI++) {
+        const aI =  atoms[_aI];
+        const isHa = type_symbol.value(aI) === 'H';
+
+        for (let i = indexPairs.offset[aI], il = indexPairs.offset[aI + 1]; i < il; ++i) {
+            const bI = indexPairs.b[i];
+            if (aI >= bI) continue;
+
+            const _bI = SortedArray.indexOf(unit.elements, bI) as StructureElement.UnitIndex;
+            if (_bI < 0) continue;
+            if (isHa && type_symbol.value(bI) === 'H') continue;
+
+            const d = edgeProps.distance[i];
+            if (d === -1 || d === void 0 || equalEps(getDistance(unit, aI, bI), d, 0.5)) {
+                atomA[atomA.length] = _aI;
+                atomB[atomB.length] = _bI;
+                order[order.length] = edgeProps.order[i];
+                flags[flags.length] = edgeProps.flag[i];
+            }
+        }
+    }
+
+    return getGraph(atomA, atomB, order, flags, atomCount, false);
+}
+
+function findBonds(unit: Unit.Atomic, props: BondComputationProps): IntraUnitBonds {
     const MAX_RADIUS = 4;
 
     const { x, y, z } = unit.model.atomicConformation;
@@ -57,7 +94,6 @@ function _computeBonds(unit: Unit.Atomic, props: BondComputationProps): IntraUni
 
     const structConn = StructConn.Provider.get(unit.model);
     const component = ComponentBond.Provider.get(unit.model);
-    const indexPairs = IndexPairBonds.Provider.get(unit.model);
 
     const atomA: StructureElement.UnitIndex[] = [];
     const atomB: StructureElement.UnitIndex[] = [];
@@ -67,31 +103,15 @@ function _computeBonds(unit: Unit.Atomic, props: BondComputationProps): IntraUni
     let lastResidue = -1;
     let componentMap: Map<string, Map<string, { flags: number, order: number }>> | undefined = void 0;
 
+    let isWatery = true, isDictionaryBased = true, isSequenced = true;
+
     const structConnAdded = __structConnAdded;
 
     for (let _aI = 0 as StructureElement.UnitIndex; _aI < atomCount; _aI++) {
         const aI =  atoms[_aI];
 
-        if (!props.forceCompute && indexPairs) {
-            const { edgeProps } = indexPairs;
-            for (let i = indexPairs.offset[aI], il = indexPairs.offset[aI + 1]; i < il; ++i) {
-                const bI = indexPairs.b[i];
-                if (aI >= bI) continue;
-
-                const _bI = SortedArray.indexOf(unit.elements, bI) as StructureElement.UnitIndex;
-                if (_bI < 0) continue;
-                if (type_symbol.value(aI) === 'H' && type_symbol.value(bI) === 'H') continue;
-
-                const d = edgeProps.distance[i];
-                if (d === -1 || equalEps(getDistance(unit, aI, bI), d, 0.5)) {
-                    atomA[atomA.length] = _aI;
-                    atomB[atomB.length] = _bI;
-                    order[order.length] = edgeProps.order[i];
-                    flags[flags.length] = edgeProps.flag[i];
-                }
-            }
-            continue; // assume `indexPairs` supplies all bonds
-        }
+        const elemA = type_symbol.value(aI);
+        if (isWatery && (elemA !== 'H' || elemA !== 'O')) isWatery = false;
 
         const structConnEntries = props.forceCompute ? void 0 : structConn && structConn.byAtomIndex.get(aI);
         let hasStructConn = false;
@@ -117,12 +137,13 @@ function _computeBonds(unit: Unit.Atomic, props: BondComputationProps): IntraUni
         }
 
         const raI = residueIndex[aI];
+        const seqIdA = label_seq_id.value(raI);
         const compId = label_comp_id.value(aI);
 
         if (!props.forceCompute && raI !== lastResidue) {
             if (!!component && component.entries.has(compId)) {
                 const entitySeq = byEntityKey[index.getEntityFromChain(chainIndex[aI])];
-                if (entitySeq && entitySeq.sequence.microHet.has(label_seq_id.value(raI))) {
+                if (entitySeq && entitySeq.sequence.microHet.has(seqIdA)) {
                     // compute for sequence positions with micro-heterogeneity
                     componentMap = void 0;
                 } else {
@@ -134,7 +155,7 @@ function _computeBonds(unit: Unit.Atomic, props: BondComputationProps): IntraUni
         }
         lastResidue = raI;
 
-        const aeI = getElementIdx(type_symbol.value(aI));
+        const aeI = getElementIdx(elemA);
         const atomIdA = label_atom_id.value(aI);
         const componentPairs = componentMap ? componentMap.get(atomIdA) : void 0;
 
@@ -194,11 +215,17 @@ function _computeBonds(unit: Unit.Atomic, props: BondComputationProps): IntraUni
                 atomB[atomB.length] = _bI;
                 order[order.length] = getIntraBondOrderFromTable(compId, atomIdA, label_atom_id.value(bI));
                 flags[flags.length] = (isMetal ? BondType.Flag.MetallicCoordination : BondType.Flag.Covalent) | BondType.Flag.Computed;
+
+                const seqIdB = label_seq_id.value(rbI);
+
+                if (seqIdA === seqIdB) isDictionaryBased = false;
+                if (Math.abs(seqIdA - seqIdB) > 1) isSequenced = false;
             }
         }
     }
 
-    return getGraph(atomA, atomB, order, flags, atomCount);
+    const canRemap = isWatery || (isDictionaryBased && isSequenced);
+    return getGraph(atomA, atomB, order, flags, atomCount, canRemap);
 }
 
 function computeIntraUnitBonds(unit: Unit.Atomic, props?: Partial<BondComputationProps>) {
@@ -208,7 +235,12 @@ function computeIntraUnitBonds(unit: Unit.Atomic, props?: Partial<BondComputatio
         //      and avoid using unit.lookup
         return IntraUnitBonds.Empty;
     }
-    return _computeBonds(unit, p);
+
+    if (!p.forceCompute && IndexPairBonds.Provider.get(unit.model)!) {
+        return findIndexPairBonds(unit);
+    } else {
+        return findBonds(unit, p);
+    }
 }
 
 export { computeIntraUnitBonds };

+ 17 - 7
src/mol-plugin-state/manager/structure/measurement.ts

@@ -13,7 +13,8 @@ import { arraySetAdd } from '../../../mol-util/array';
 import { PluginStateObject } from '../../objects';
 import { StatefulPluginComponent } from '../../component';
 import { ParamDefinition as PD } from '../../../mol-util/param-definition';
-import { MeasurementRepresentationCommonTextParams } from '../../../mol-repr/shape/loci/common';
+import { MeasurementRepresentationCommonTextParams, LociLabelTextParams } from '../../../mol-repr/shape/loci/common';
+import { LineParams } from '../../../mol-repr/structure/representation/line';
 
 export { StructureMeasurementManager };
 
@@ -40,7 +41,9 @@ export interface StructureMeasurementManagerState {
 type StructureMeasurementManagerAddOptions = {
     customText?: string,
     selectionTags?: string | string[],
-    reprTags?: string | string[]
+    reprTags?: string | string[],
+    lineParams?: Partial<PD.Values<LineParams>>,
+    labelParams?: Partial<PD.Values<LociLabelTextParams>>
 }
 
 class StructureMeasurementManager extends StatefulPluginComponent<StructureMeasurementManagerState>  {
@@ -108,7 +111,9 @@ class StructureMeasurementManager extends StatefulPluginComponent<StructureMeasu
             .apply(StateTransforms.Representation.StructureSelectionsDistance3D, {
                 customText: options?.customText || '',
                 unitLabel: this.state.options.distanceUnitLabel,
-                textColor: this.state.options.textColor
+                textColor: this.state.options.textColor,
+                ...options?.lineParams,
+                ...options?.labelParams
             }, { tags: options?.reprTags });
 
         const state = this.plugin.state.data;
@@ -139,7 +144,9 @@ class StructureMeasurementManager extends StatefulPluginComponent<StructureMeasu
             }, { dependsOn, tags: options?.selectionTags })
             .apply(StateTransforms.Representation.StructureSelectionsAngle3D, {
                 customText: options?.customText || '',
-                textColor: this.state.options.textColor
+                textColor: this.state.options.textColor,
+                ...options?.lineParams,
+                ...options?.labelParams
             }, { tags: options?.reprTags });
 
         const state = this.plugin.state.data;
@@ -173,14 +180,16 @@ class StructureMeasurementManager extends StatefulPluginComponent<StructureMeasu
             }, { dependsOn, tags: options?.selectionTags })
             .apply(StateTransforms.Representation.StructureSelectionsDihedral3D, {
                 customText: options?.customText || '',
-                textColor: this.state.options.textColor
+                textColor: this.state.options.textColor,
+                ...options?.lineParams,
+                ...options?.labelParams
             }, { tags: options?.reprTags });
 
         const state = this.plugin.state.data;
         await PluginCommands.State.Update(this.plugin, { state, tree: update, options: { doNotLogTiming: true } });
     }
 
-    async addLabel(a: StructureElement.Loci, options?: Omit<StructureMeasurementManagerAddOptions, 'customText'>) {
+    async addLabel(a: StructureElement.Loci, options?: Omit<StructureMeasurementManagerAddOptions, 'customText' | 'lineParams'>) {
         const cellA = this.plugin.helpers.substructureParent.get(a.structure);
 
         if (!cellA) return;
@@ -197,7 +206,8 @@ class StructureMeasurementManager extends StatefulPluginComponent<StructureMeasu
                 label: 'Label'
             }, { dependsOn, tags: options?.selectionTags })
             .apply(StateTransforms.Representation.StructureSelectionsLabel3D, {
-                textColor: this.state.options.textColor
+                textColor: this.state.options.textColor,
+                ...options?.labelParams
             }, { tags: options?.reprTags });
 
         const state = this.plugin.state.data;

+ 3 - 10
src/mol-repr/shape/loci/angle.ts

@@ -24,7 +24,7 @@ import { transformPrimitive } from '../../../mol-geo/primitive/primitive';
 import { MarkerActions, MarkerAction } from '../../../mol-util/marker-action';
 import { angleLabel } from '../../../mol-theme/label';
 import { Sphere3D } from '../../../mol-math/geometry';
-import { MeasurementRepresentationCommonTextParams } from './common';
+import { LociLabelTextParams } from './common';
 
 export interface AngleData {
     triples: Loci.Bundle<3>[]
@@ -61,25 +61,18 @@ const SectorParams = {
 };
 type SectorParams = typeof SectorParams
 
-const TextParams = {
-    ...Text.Params,
-    ...MeasurementRepresentationCommonTextParams,
-    borderWidth: PD.Numeric(0.2, { min: 0, max: 0.5, step: 0.01 })
-};
-type TextParams = typeof TextParams
-
 const AngleVisuals = {
     'vectors': (ctx: RepresentationContext, getParams: RepresentationParamsGetter<AngleData, VectorsParams>) => ShapeRepresentation(getVectorsShape, Lines.Utils, { modifyState: s => ({ ...s, pickable: false }) }),
     'arc': (ctx: RepresentationContext, getParams: RepresentationParamsGetter<AngleData, ArcParams>) => ShapeRepresentation(getArcShape, Lines.Utils, { modifyState: s => ({ ...s, pickable: false }) }),
     'sector': (ctx: RepresentationContext, getParams: RepresentationParamsGetter<AngleData, SectorParams>) => ShapeRepresentation(getSectorShape, Mesh.Utils, { modifyProps: p => ({ ...p, alpha: p.sectorOpacity }), modifyState: s => ({ ...s, markerActions: MarkerActions.Highlighting }) }),
-    'text': (ctx: RepresentationContext, getParams: RepresentationParamsGetter<AngleData, TextParams>) => ShapeRepresentation(getTextShape, Text.Utils, { modifyState: s => ({ ...s, markerActions: MarkerAction.None }) }),
+    'text': (ctx: RepresentationContext, getParams: RepresentationParamsGetter<AngleData, LociLabelTextParams>) => ShapeRepresentation(getTextShape, Text.Utils, { modifyState: s => ({ ...s, markerActions: MarkerAction.None }) }),
 };
 
 export const AngleParams = {
     ...VectorsParams,
     ...ArcParams,
     ...SectorParams,
-    ...TextParams,
+    ...LociLabelTextParams,
     visuals: PD.MultiSelect(['vectors', 'sector', 'text'], PD.objectToOptions(AngleVisuals)),
 };
 export type AngleParams = typeof AngleParams

+ 9 - 1
src/mol-repr/shape/loci/common.ts

@@ -7,9 +7,17 @@
 
 import { ParamDefinition as PD } from '../../../mol-util/param-definition';
 import { ColorNames } from '../../../mol-util/color/names';
+import { Text } from '../../../mol-geo/geometry/text/text';
 
 export const MeasurementRepresentationCommonTextParams = {
     customText: PD.Text('', { label: 'Text', description: 'Override the label with custom value.' }),
     textColor: PD.Color(ColorNames.black, { isEssential: true }),
     textSize: PD.Numeric(0.5, { min: 0.1, max: 5, step: 0.1 }, { isEssential: true }),
-};
+};
+
+export const LociLabelTextParams = {
+    ...Text.Params,
+    ...MeasurementRepresentationCommonTextParams,
+    borderWidth: PD.Numeric(0.2, { min: 0, max: 0.5, step: 0.01 })
+};
+export type LociLabelTextParams = typeof LociLabelTextParams

+ 3 - 10
src/mol-repr/shape/loci/dihedral.ts

@@ -23,7 +23,7 @@ import { Circle } from '../../../mol-geo/primitive/circle';
 import { transformPrimitive } from '../../../mol-geo/primitive/primitive';
 import { MarkerActions, MarkerAction } from '../../../mol-util/marker-action';
 import { dihedralLabel } from '../../../mol-theme/label';
-import { MeasurementRepresentationCommonTextParams } from './common';
+import { LociLabelTextParams } from './common';
 import { Sphere3D } from '../../../mol-math/geometry';
 
 export interface DihedralData {
@@ -66,20 +66,13 @@ const SectorParams = {
 };
 type SectorParams = typeof SectorParams
 
-const TextParams = {
-    ...Text.Params,
-    borderWidth: PD.Numeric(0.2, { min: 0, max: 0.5, step: 0.01 }),
-    ...MeasurementRepresentationCommonTextParams
-};
-type TextParams = typeof TextParams
-
 const DihedralVisuals = {
     'vectors': (ctx: RepresentationContext, getParams: RepresentationParamsGetter<DihedralData, VectorsParams>) => ShapeRepresentation(getVectorsShape, Lines.Utils, { modifyState: s => ({ ...s, pickable: false }) }),
     'extenders': (ctx: RepresentationContext, getParams: RepresentationParamsGetter<DihedralData, ExtendersParams>) => ShapeRepresentation(getExtendersShape, Lines.Utils, { modifyState: s => ({ ...s, pickable: false }) }),
     'connector': (ctx: RepresentationContext, getParams: RepresentationParamsGetter<DihedralData, ExtendersParams>) => ShapeRepresentation(getConnectorShape, Lines.Utils, { modifyState: s => ({ ...s, pickable: false }) }),
     'arc': (ctx: RepresentationContext, getParams: RepresentationParamsGetter<DihedralData, ArcParams>) => ShapeRepresentation(getArcShape, Lines.Utils, { modifyState: s => ({ ...s, pickable: false }) }),
     'sector': (ctx: RepresentationContext, getParams: RepresentationParamsGetter<DihedralData, SectorParams>) => ShapeRepresentation(getSectorShape, Mesh.Utils, { modifyProps: p => ({ ...p, alpha: p.sectorOpacity }), modifyState: s => ({ ...s, markerActions: MarkerActions.Highlighting }) }),
-    'text': (ctx: RepresentationContext, getParams: RepresentationParamsGetter<DihedralData, TextParams>) => ShapeRepresentation(getTextShape, Text.Utils, { modifyState: s => ({ ...s, markerActions: MarkerAction.None }) }),
+    'text': (ctx: RepresentationContext, getParams: RepresentationParamsGetter<DihedralData, LociLabelTextParams>) => ShapeRepresentation(getTextShape, Text.Utils, { modifyState: s => ({ ...s, markerActions: MarkerAction.None }) }),
 };
 
 export const DihedralParams = {
@@ -87,7 +80,7 @@ export const DihedralParams = {
     ...ExtendersParams,
     ...ArcParams,
     ...SectorParams,
-    ...TextParams,
+    ...LociLabelTextParams,
     visuals: PD.MultiSelect(['extenders', 'sector', 'text'], PD.objectToOptions(DihedralVisuals)),
 };
 export type DihedralParams = typeof DihedralParams

+ 2 - 4
src/mol-repr/shape/loci/distance.ts

@@ -18,7 +18,7 @@ import { TextBuilder } from '../../../mol-geo/geometry/text/text-builder';
 import { Vec3 } from '../../../mol-math/linear-algebra';
 import { MarkerActions, MarkerAction } from '../../../mol-util/marker-action';
 import { distanceLabel } from '../../../mol-theme/label';
-import { MeasurementRepresentationCommonTextParams } from './common';
+import { LociLabelTextParams } from './common';
 import { Sphere3D } from '../../../mol-math/geometry';
 
 export interface DistanceData {
@@ -40,10 +40,8 @@ const LineParams = {
 type LineParams = typeof LineParams
 
 const TextParams = {
-    ...Text.Params,
+    ...LociLabelTextParams,
     ...SharedParams,
-    borderWidth: PD.Numeric(0.2, { min: 0, max: 0.5, step: 0.01 }),
-    ...MeasurementRepresentationCommonTextParams
 };
 type TextParams = typeof TextParams
 

+ 4 - 5
src/mol-repr/shape/loci/label.ts

@@ -14,16 +14,14 @@ import { Shape } from '../../../mol-model/shape';
 import { TextBuilder } from '../../../mol-geo/geometry/text/text-builder';
 import { Sphere3D } from '../../../mol-math/geometry';
 import { lociLabel } from '../../../mol-theme/label';
-import { MeasurementRepresentationCommonTextParams } from './common';
+import { LociLabelTextParams } from './common';
 
 export interface LabelData {
     infos: { loci: Loci, label?: string }[]
 }
 
 const TextParams = {
-    ...Text.Params,
-    borderWidth: PD.Numeric(0.2, { min: 0, max: 0.5, step: 0.01 }),
-    ...MeasurementRepresentationCommonTextParams,
+    ...LociLabelTextParams,
     offsetZ: PD.Numeric(2, { min: 0, max: 10, step: 0.1 }),
 };
 type TextParams = typeof TextParams
@@ -34,6 +32,7 @@ const LabelVisuals = {
 
 export const LabelParams = {
     ...TextParams,
+    scaleByRadius: PD.Boolean(true),
     visuals: PD.MultiSelect(['text'], PD.objectToOptions(LabelVisuals)),
 };
 
@@ -62,7 +61,7 @@ function buildText(data: LabelData, props: LabelProps, text?: Text): Text {
         if (!sphere) continue;
         const { center, radius } = sphere;
         const text = label(info, true);
-        builder.add(text, center[0], center[1], center[2], radius / 0.9, Math.max(1, radius), i);
+        builder.add(text, center[0], center[1], center[2], props.scaleByRadius ? radius / 0.9 : 0, props.scaleByRadius ? Math.max(1, radius) : 1, i);
     }
     return builder.getText();
 }

+ 31 - 9
src/mol-repr/structure/visual/bond-inter-unit-cylinder.ts

@@ -38,6 +38,24 @@ function createInterUnitBondCylinderMesh(ctx: VisualContext, structure: Structur
 
     if (!edgeCount) return Mesh.createEmpty(mesh);
 
+    const delta = Vec3();
+
+    const radiusA = (edgeIndex: number) => {
+        const b = edges[edgeIndex];
+        tmpLoc.structure = structure;
+        tmpLoc.unit = structure.unitMap.get(b.unitA);
+        tmpLoc.element = tmpLoc.unit.elements[b.indexA];
+        return theme.size.size(tmpLoc) * sizeFactor;
+    };
+
+    const radiusB = (edgeIndex: number) => {
+        const b = edges[edgeIndex];
+        tmpLoc.structure = structure;
+        tmpLoc.unit = structure.unitMap.get(b.unitB);
+        tmpLoc.element = tmpLoc.unit.elements[b.indexB];
+        return theme.size.size(tmpLoc) * sizeFactor;
+    };
+
     const builderProps = {
         linkCount: edgeCount,
         referencePosition: (edgeIndex: number) => {
@@ -63,8 +81,20 @@ function createInterUnitBondCylinderMesh(ctx: VisualContext, structure: Structur
             const b = edges[edgeIndex];
             const uA = structure.unitMap.get(b.unitA);
             const uB = structure.unitMap.get(b.unitB);
+
+            const rA = radiusA(edgeIndex), rB = radiusB(edgeIndex);
+            const r = Math.min(rA, rB) * sizeAspectRatio;
+            const oA = Math.sqrt(Math.max(0, rA * rA - r * r)) - 0.05;
+            const oB = Math.sqrt(Math.max(0, rB * rB - r * r)) - 0.05;
+
             uA.conformation.position(uA.elements[b.indexA], posA);
             uB.conformation.position(uB.elements[b.indexB], posB);
+
+            if (oA <= 0.01 && oB <= 0.01) return;
+
+            Vec3.normalize(delta, Vec3.sub(delta, posB, posA));
+            Vec3.scaleAndAdd(posA, posA, delta, oA);
+            Vec3.scaleAndAdd(posB, posB, delta, -oB);
         },
         style: (edgeIndex: number) => {
             const o = edges[edgeIndex].props.order;
@@ -81,15 +111,7 @@ function createInterUnitBondCylinderMesh(ctx: VisualContext, structure: Structur
             }
         },
         radius: (edgeIndex: number) => {
-            const b = edges[edgeIndex];
-            tmpLoc.structure = structure;
-            tmpLoc.unit = structure.unitMap.get(b.unitA);
-            tmpLoc.element = tmpLoc.unit.elements[b.indexA];
-            const sizeA = theme.size.size(tmpLoc);
-            tmpLoc.unit = structure.unitMap.get(b.unitB);
-            tmpLoc.element = tmpLoc.unit.elements[b.indexB];
-            const sizeB = theme.size.size(tmpLoc);
-            return Math.min(sizeA, sizeB) * sizeFactor * sizeAspectRatio;
+            return Math.min(radiusA(edgeIndex), radiusB(edgeIndex)) * sizeAspectRatio;
         },
         ignore: makeInterBondIgnoreTest(structure, props)
     };

+ 23 - 6
src/mol-repr/structure/visual/bond-intra-unit-cylinder.ts

@@ -36,9 +36,19 @@ function createIntraUnitBondCylinderMesh(ctx: VisualContext, unit: Unit, structu
 
     if (!edgeCount) return Mesh.createEmpty(mesh);
 
-    const vRef = Vec3();
+    const vRef = Vec3(), delta = Vec3();
     const pos = unit.conformation.invariantPosition;
 
+    const radiusA = (edgeIndex: number) => {
+        location.element = elements[a[edgeIndex]];
+        return theme.size.size(location) * sizeFactor;
+    };
+
+    const radiusB = (edgeIndex: number) => {
+        location.element = elements[b[edgeIndex]];
+        return theme.size.size(location) * sizeFactor;
+    };
+
     const builderProps = {
         linkCount: edgeCount * 2,
         referencePosition: (edgeIndex: number) => {
@@ -59,8 +69,19 @@ function createIntraUnitBondCylinderMesh(ctx: VisualContext, unit: Unit, structu
             return null;
         },
         position: (posA: Vec3, posB: Vec3, edgeIndex: number) => {
+            const rA = radiusA(edgeIndex), rB = radiusB(edgeIndex);
+            const r = Math.min(rA, rB) * sizeAspectRatio;
+            const oA = Math.sqrt(Math.max(0, rA * rA - r * r)) - 0.05;
+            const oB = Math.sqrt(Math.max(0, rB * rB - r * r)) - 0.05;
+
             pos(elements[a[edgeIndex]], posA);
             pos(elements[b[edgeIndex]], posB);
+
+            if (oA <= 0.01 && oB <= 0.01) return;
+
+            Vec3.normalize(delta, Vec3.sub(delta, posB, posA));
+            Vec3.scaleAndAdd(posA, posA, delta, oA);
+            Vec3.scaleAndAdd(posB, posB, delta, -oB);
         },
         style: (edgeIndex: number) => {
             const o = _order[edgeIndex];
@@ -77,11 +98,7 @@ function createIntraUnitBondCylinderMesh(ctx: VisualContext, unit: Unit, structu
             }
         },
         radius: (edgeIndex: number) => {
-            location.element = elements[a[edgeIndex]];
-            const sizeA = theme.size.size(location);
-            location.element = elements[b[edgeIndex]];
-            const sizeB = theme.size.size(location);
-            return Math.min(sizeA, sizeB) * sizeFactor * sizeAspectRatio;
+            return Math.min(radiusA(edgeIndex), radiusB(edgeIndex)) * sizeAspectRatio;
         },
         ignore: makeIntraBondIgnoreTest(unit, props)
     };

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

@@ -65,7 +65,7 @@ export function calculateShiftDir (out: Vec3, v1: Vec3, v2: Vec3, v3: Vec3 | nul
 export interface LinkBuilderProps {
     linkCount: number
     position: (posA: Vec3, posB: Vec3, edgeIndex: number) => void
-    radius: (edgeIndex: number) => number
+    radius: (edgeIndex: number) => number,
     referencePosition?: (edgeIndex: number) => Vec3 | null
     style?: (edgeIndex: number) => LinkStyle
     ignore?: (edgeIndex: number) => boolean