Procházet zdrojové kódy

line representation

Alexander Rose před 4 roky
rodič
revize
eff80ad5ff

+ 3 - 3
src/extensions/rcsb/validation-report/representation.ts

@@ -15,7 +15,7 @@ import { Interval } from '../../../mol-data/int';
 import { RepresentationContext, RepresentationParamsGetter, Representation } from '../../../mol-repr/representation';
 import { UnitsRepresentation, StructureRepresentation, StructureRepresentationStateBuilder, StructureRepresentationProvider, ComplexRepresentation } from '../../../mol-repr/structure/representation';
 import { VisualContext } from '../../../mol-repr/visual';
-import { createLinkCylinderMesh, LinkCylinderParams, LinkCylinderStyle } from '../../../mol-repr/structure/visual/util/link';
+import { createLinkCylinderMesh, LinkCylinderParams, LinkStyle } from '../../../mol-repr/structure/visual/util/link';
 import { UnitsMeshParams, UnitsVisual, UnitsMeshVisual, StructureGroup } from '../../../mol-repr/structure/units-visual';
 import { VisualUpdateState } from '../../../mol-repr/util';
 import { LocationIterator } from '../../../mol-geo/util/location-iterator';
@@ -50,7 +50,7 @@ function createIntraUnitClashCylinderMesh(ctx: VisualContext, unit: Unit, struct
             pos(elements[a[edgeIndex]], posA);
             pos(elements[b[edgeIndex]], posB);
         },
-        style: (edgeIndex: number) => LinkCylinderStyle.Disk,
+        style: (edgeIndex: number) => LinkStyle.Disk,
         radius: (edgeIndex: number) => magnitude[edgeIndex] * sizeFactor,
     };
 
@@ -163,7 +163,7 @@ function createInterUnitClashCylinderMesh(ctx: VisualContext, structure: Structu
             uA.conformation.position(uA.elements[b.indexA], posA);
             uB.conformation.position(uB.elements[b.indexB], posB);
         },
-        style: (edgeIndex: number) => LinkCylinderStyle.Disk,
+        style: (edgeIndex: number) => LinkStyle.Disk,
         radius: (edgeIndex: number) => edges[edgeIndex].props.magnitude * sizeFactor
     };
 

+ 2 - 2
src/mol-model-props/computed/representations/interactions-inter-unit-cylinder.ts

@@ -10,7 +10,7 @@ import { Structure, StructureElement } from '../../../mol-model/structure';
 import { Theme } from '../../../mol-theme/theme';
 import { Mesh } from '../../../mol-geo/geometry/mesh/mesh';
 import { Vec3 } from '../../../mol-math/linear-algebra';
-import { createLinkCylinderMesh, LinkCylinderParams, LinkCylinderStyle } from '../../../mol-repr/structure/visual/util/link';
+import { createLinkCylinderMesh, LinkCylinderParams, LinkStyle } from '../../../mol-repr/structure/visual/util/link';
 import { ComplexMeshParams, ComplexVisual, ComplexMeshVisual } from '../../../mol-repr/structure/complex-visual';
 import { VisualUpdateState } from '../../../mol-repr/util';
 import { PickingId } from '../../../mol-geo/geometry/picking';
@@ -47,7 +47,7 @@ function createInterUnitInteractionCylinderMesh(ctx: VisualContext, structure: S
             Vec3.set(posB, fB.x[indexB], fB.y[indexB], fB.z[indexB]);
             Vec3.transformMat4(posB, posB, unitB.conformation.operator.matrix);
         },
-        style: (edgeIndex: number) => LinkCylinderStyle.Dashed,
+        style: (edgeIndex: number) => LinkStyle.Dashed,
         radius: (edgeIndex: number) => {
             const b = edges[edgeIndex];
             const fA = unitsFeatures.get(b.unitA.id);

+ 2 - 2
src/mol-model-props/computed/representations/interactions-intra-unit-cylinder.ts

@@ -14,7 +14,7 @@ import { PickingId } from '../../../mol-geo/geometry/picking';
 import { VisualContext } from '../../../mol-repr/visual';
 import { Theme } from '../../../mol-theme/theme';
 import { InteractionsProvider } from '../interactions';
-import { createLinkCylinderMesh, LinkCylinderParams, LinkCylinderStyle } from '../../../mol-repr/structure/visual/util/link';
+import { createLinkCylinderMesh, LinkCylinderParams, LinkStyle } from '../../../mol-repr/structure/visual/util/link';
 import { UnitsMeshParams, UnitsVisual, UnitsMeshVisual, StructureGroup } from '../../../mol-repr/structure/units-visual';
 import { VisualUpdateState } from '../../../mol-repr/util';
 import { LocationIterator } from '../../../mol-geo/util/location-iterator';
@@ -42,7 +42,7 @@ async function createIntraUnitInteractionsCylinderMesh(ctx: VisualContext, unit:
             Vec3.set(posA, x[a[edgeIndex]], y[a[edgeIndex]], z[a[edgeIndex]]);
             Vec3.set(posB, x[b[edgeIndex]], y[b[edgeIndex]], z[b[edgeIndex]]);
         },
-        style: (edgeIndex: number) => LinkCylinderStyle.Dashed,
+        style: (edgeIndex: number) => LinkStyle.Dashed,
         radius: (edgeIndex: number) => {
             location.element = unit.elements[members[offsets[a[edgeIndex]]]];
             const sizeA = theme.size.size(location);

+ 20 - 1
src/mol-repr/structure/complex-visual.ts

@@ -29,8 +29,9 @@ import { Text } from '../../mol-geo/geometry/text/text';
 import { SizeTheme } from '../../mol-theme/size';
 import { DirectVolume } from '../../mol-geo/geometry/direct-volume/direct-volume';
 import { createMarkers } from '../../mol-geo/geometry/marker-data';
-import { StructureParams, StructureMeshParams, StructureTextParams, StructureDirectVolumeParams } from './params';
+import { StructureParams, StructureMeshParams, StructureTextParams, StructureDirectVolumeParams, StructureLinesParams } from './params';
 import { Clipping } from '../../mol-theme/clipping';
+import { Lines } from '../../mol-geo/geometry/lines/lines';
 
 export interface  ComplexVisual<P extends StructureParams> extends Visual<Structure, P> { }
 
@@ -246,6 +247,24 @@ export function ComplexMeshVisual<P extends ComplexMeshParams>(builder: ComplexM
     }, materialId);
 }
 
+// lines
+
+export const ComplexLinesParams = { ...StructureLinesParams, ...StructureParams };
+export type ComplexLinesParams = typeof ComplexLinesParams
+
+export interface ComplexLinesVisualBuilder<P extends ComplexLinesParams> extends ComplexVisualBuilder<P, Lines> { }
+
+export function ComplexLinesVisual<P extends ComplexLinesParams>(builder: ComplexLinesVisualBuilder<P>, materialId: number): ComplexVisual<P> {
+    return ComplexVisual<Lines, P>({
+        ...builder,
+        setUpdateState: (state: VisualUpdateState, newProps: PD.Values<P>, currentProps: PD.Values<P>, newTheme: Theme, currentTheme: Theme, newStructure: Structure, currentStructure: Structure) => {
+            builder.setUpdateState(state, newProps, currentProps, newTheme, currentTheme, newStructure, currentStructure);
+            if (!SizeTheme.areEqual(newTheme.size, currentTheme.size)) state.updateSize = true;
+        },
+        geometryUtils: Lines.Utils
+    }, materialId);
+}
+
 // text
 
 export const ComplexTextParams = { ...StructureTextParams, ...StructureParams };

+ 2 - 0
src/mol-repr/structure/registry.ts

@@ -19,6 +19,7 @@ import { OrientationRepresentationProvider } from './representation/orientation'
 import { PointRepresentationProvider } from './representation/point';
 import { PuttyRepresentationProvider } from './representation/putty';
 import { SpacefillRepresentationProvider } from './representation/spacefill';
+import { LineRepresentationProvider } from './representation/line';
 
 export class StructureRepresentationRegistry extends RepresentationRegistry<Structure, StructureRepresentationState> {
     constructor() {
@@ -39,6 +40,7 @@ export namespace StructureRepresentationRegistry {
         'gaussian-surface': GaussianSurfaceRepresentationProvider,
         // 'gaussian-volume': GaussianVolumeRepresentationProvider, // TODO disabled for now, needs more work
         'label': LabelRepresentationProvider,
+        'line': LineRepresentationProvider,
         'molecular-surface': MolecularSurfaceRepresentationProvider,
         'orientation': OrientationRepresentationProvider,
         'point': PointRepresentationProvider,

+ 6 - 6
src/mol-repr/structure/representation/ball-and-stick.ts

@@ -5,8 +5,8 @@
  */
 
 import { getElementSphereVisual, ElementSphereParams } from '../visual/element-sphere';
-import { IntraUnitBondVisual, IntraUnitBondParams } from '../visual/bond-intra-unit-cylinder';
-import { InterUnitBondVisual, InterUnitBondParams } from '../visual/bond-inter-unit-cylinder';
+import { IntraUnitBondCylinderVisual, IntraUnitBondCylinderParams } from '../visual/bond-intra-unit-cylinder';
+import { InterUnitBondCylinderVisual, InterUnitBondCylinderParams } from '../visual/bond-inter-unit-cylinder';
 import { ParamDefinition as PD } from '../../../mol-util/param-definition';
 import { UnitsRepresentation } from '../units-representation';
 import { ComplexRepresentation } from '../complex-representation';
@@ -18,14 +18,14 @@ import { getUnitKindsParam } from '../params';
 
 const BallAndStickVisuals = {
     'element-sphere': (ctx: RepresentationContext, getParams: RepresentationParamsGetter<Structure, ElementSphereParams>) => UnitsRepresentation('Element sphere mesh', ctx, getParams, getElementSphereVisual(ctx.webgl)),
-    'intra-bond': (ctx: RepresentationContext, getParams: RepresentationParamsGetter<Structure, IntraUnitBondParams>) => UnitsRepresentation('Intra-unit bond cylinder', ctx, getParams, IntraUnitBondVisual),
-    'inter-bond': (ctx: RepresentationContext, getParams: RepresentationParamsGetter<Structure, InterUnitBondParams>) => ComplexRepresentation('Inter-unit bond cylinder', ctx, getParams, InterUnitBondVisual),
+    'intra-bond': (ctx: RepresentationContext, getParams: RepresentationParamsGetter<Structure, IntraUnitBondCylinderParams>) => UnitsRepresentation('Intra-unit bond cylinder', ctx, getParams, IntraUnitBondCylinderVisual),
+    'inter-bond': (ctx: RepresentationContext, getParams: RepresentationParamsGetter<Structure, InterUnitBondCylinderParams>) => ComplexRepresentation('Inter-unit bond cylinder', ctx, getParams, InterUnitBondCylinderVisual),
 };
 
 export const BallAndStickParams = {
     ...ElementSphereParams,
-    ...IntraUnitBondParams,
-    ...InterUnitBondParams,
+    ...IntraUnitBondCylinderParams,
+    ...InterUnitBondCylinderParams,
     unitKinds: getUnitKindsParam(['atomic']),
     sizeFactor: PD.Numeric(0.15, { min: 0.01, max: 10, step: 0.01 }),
     sizeAspectRatio: PD.Numeric(2 / 3, { min: 0.01, max: 3, step: 0.01 }),

+ 6 - 6
src/mol-repr/structure/representation/ellipsoid.ts

@@ -11,20 +11,20 @@ import { Structure } from '../../../mol-model/structure';
 import { UnitsRepresentation, StructureRepresentation, StructureRepresentationStateBuilder, StructureRepresentationProvider, ComplexRepresentation } from '../../../mol-repr/structure/representation';
 import { EllipsoidMeshParams, EllipsoidMeshVisual } from '../visual/ellipsoid-mesh';
 import { AtomSiteAnisotrop } from '../../../mol-model-formats/structure/property/anisotropic';
-import { IntraUnitBondParams, IntraUnitBondVisual } from '../visual/bond-intra-unit-cylinder';
-import { InterUnitBondParams, InterUnitBondVisual } from '../visual/bond-inter-unit-cylinder';
+import { IntraUnitBondCylinderParams, IntraUnitBondCylinderVisual } from '../visual/bond-intra-unit-cylinder';
+import { InterUnitBondCylinderParams, InterUnitBondCylinderVisual } from '../visual/bond-inter-unit-cylinder';
 import { getUnitKindsParam } from '../params';
 
 const EllipsoidVisuals = {
     'ellipsoid-mesh': (ctx: RepresentationContext, getParams: RepresentationParamsGetter<Structure, EllipsoidMeshParams>) => UnitsRepresentation('Ellipsoid Mesh', ctx, getParams, EllipsoidMeshVisual),
-    'intra-bond': (ctx: RepresentationContext, getParams: RepresentationParamsGetter<Structure, IntraUnitBondParams>) => UnitsRepresentation('Intra-unit bond cylinder', ctx, getParams, IntraUnitBondVisual),
-    'inter-bond': (ctx: RepresentationContext, getParams: RepresentationParamsGetter<Structure, InterUnitBondParams>) => ComplexRepresentation('Inter-unit bond cylinder', ctx, getParams, InterUnitBondVisual),
+    'intra-bond': (ctx: RepresentationContext, getParams: RepresentationParamsGetter<Structure, IntraUnitBondCylinderParams>) => UnitsRepresentation('Intra-unit bond cylinder', ctx, getParams, IntraUnitBondCylinderVisual),
+    'inter-bond': (ctx: RepresentationContext, getParams: RepresentationParamsGetter<Structure, InterUnitBondCylinderParams>) => ComplexRepresentation('Inter-unit bond cylinder', ctx, getParams, InterUnitBondCylinderVisual),
 };
 
 export const EllipsoidParams = {
     ...EllipsoidMeshParams,
-    ...IntraUnitBondParams,
-    ...InterUnitBondParams,
+    ...IntraUnitBondCylinderParams,
+    ...InterUnitBondCylinderParams,
     unitKinds: getUnitKindsParam(['atomic']),
     sizeFactor: PD.Numeric(1, { min: 0.01, max: 10, step: 0.01 }),
     sizeAspectRatio: PD.Numeric(0.1, { min: 0.01, max: 3, step: 0.01 }),

+ 50 - 0
src/mol-repr/structure/representation/line.ts

@@ -0,0 +1,50 @@
+/**
+ * Copyright (c) 2020 mol* contributors, licensed under MIT, See LICENSE file for more info.
+ *
+ * @author Alexander Rose <alexander.rose@weirdbyte.de>
+ */
+
+import { IntraUnitBondLineVisual, IntraUnitBondLineParams } from '../visual/bond-intra-unit-line';
+import { InterUnitBondLineVisual, InterUnitBondLineParams } from '../visual/bond-inter-unit-line';
+import { ParamDefinition as PD } from '../../../mol-util/param-definition';
+import { UnitsRepresentation } from '../units-representation';
+import { ComplexRepresentation } from '../complex-representation';
+import { StructureRepresentation, StructureRepresentationProvider, StructureRepresentationStateBuilder } from '../representation';
+import { Representation, RepresentationParamsGetter, RepresentationContext } from '../../../mol-repr/representation';
+import { ThemeRegistryContext } from '../../../mol-theme/theme';
+import { Structure } from '../../../mol-model/structure';
+import { getUnitKindsParam } from '../params';
+
+const LineVisuals = {
+    'intra-bond': (ctx: RepresentationContext, getParams: RepresentationParamsGetter<Structure, IntraUnitBondLineParams>) => UnitsRepresentation('Intra-unit bond line', ctx, getParams, IntraUnitBondLineVisual),
+    'inter-bond': (ctx: RepresentationContext, getParams: RepresentationParamsGetter<Structure, InterUnitBondLineParams>) => ComplexRepresentation('Inter-unit bond line', ctx, getParams, InterUnitBondLineVisual),
+};
+
+export const LineParams = {
+    ...IntraUnitBondLineParams,
+    ...InterUnitBondLineParams,
+    sizeFactor: PD.Numeric(1.0, { min: 0.01, max: 10, step: 0.01 }),
+    unitKinds: getUnitKindsParam(['atomic']),
+    visuals: PD.MultiSelect(['intra-bond', 'inter-bond'], PD.objectToOptions(LineVisuals))
+};
+export type LineParams = typeof LineParams
+export function getLineParams(ctx: ThemeRegistryContext, structure: Structure) {
+    return PD.clone(LineParams);
+}
+
+export type LineRepresentation = StructureRepresentation<LineParams>
+export function LineRepresentation(ctx: RepresentationContext, getParams: RepresentationParamsGetter<Structure, LineParams>): LineRepresentation {
+    return Representation.createMulti('Line', ctx, getParams, StructureRepresentationStateBuilder, LineVisuals as unknown as Representation.Def<Structure, LineParams>);
+}
+
+export const LineRepresentationProvider = StructureRepresentationProvider({
+    name: 'line',
+    label: 'Line',
+    description: 'Displays bonds as lines.',
+    factory: LineRepresentation,
+    getParams: getLineParams,
+    defaultValues: PD.getDefaultValues(LineParams),
+    defaultColorTheme: { name: 'element-symbol' },
+    defaultSizeTheme: { name: 'uniform' },
+    isApplicable: (structure: Structure) => structure.elementCount > 0
+});

+ 15 - 75
src/mol-repr/structure/visual/bond-inter-unit-cylinder.ts

@@ -11,15 +11,12 @@ import { Theme } from '../../../mol-theme/theme';
 import { Mesh } from '../../../mol-geo/geometry/mesh/mesh';
 import { Vec3 } from '../../../mol-math/linear-algebra';
 import { BitFlags, arrayEqual } from '../../../mol-util';
-import { createLinkCylinderMesh, LinkCylinderStyle } from './util/link';
+import { createLinkCylinderMesh, LinkStyle } from './util/link';
 import { ComplexMeshParams, ComplexVisual, ComplexMeshVisual } from '../complex-visual';
 import { VisualUpdateState } from '../../util';
-import { PickingId } from '../../../mol-geo/geometry/picking';
-import { EmptyLoci, Loci } from '../../../mol-model/loci';
-import { Interval, OrderedSet } from '../../../mol-data/int';
 import { isHydrogen } from './util/common';
 import { BondType } from '../../../mol-model/structure/model/types';
-import { ignoreBondType, BondCylinderParams, BondIterator } from './util/bond';
+import { ignoreBondType, BondCylinderParams, BondIterator, getInterBondLoci, eachInterBond } from './util/bond';
 
 const tmpRefPosBondIt = new Bond.ElementBondIterator();
 function setRefPosition(pos: Vec3, structure: Structure, unit: Unit.Atomic, index: StructureElement.UnitIndex) {
@@ -35,7 +32,7 @@ function setRefPosition(pos: Vec3, structure: Structure, unit: Unit.Atomic, inde
 const tmpRef = Vec3();
 const tmpLoc = StructureElement.Location.create(void 0);
 
-function createInterUnitBondCylinderMesh(ctx: VisualContext, structure: Structure, theme: Theme, props: PD.Values<InterUnitBondParams>, mesh?: Mesh) {
+function createInterUnitBondCylinderMesh(ctx: VisualContext, structure: Structure, theme: Theme, props: PD.Values<InterUnitBondCylinderParams>, mesh?: Mesh) {
     const bonds = structure.interUnitBonds;
     const { edgeCount, edges } = bonds;
     const { sizeFactor, sizeAspectRatio, ignoreHydrogens, includeTypes, excludeTypes } = props;
@@ -79,13 +76,13 @@ function createInterUnitBondCylinderMesh(ctx: VisualContext, structure: Structur
             const f = BitFlags.create(edges[edgeIndex].props.flag);
             if (BondType.is(f, BondType.Flag.MetallicCoordination) || BondType.is(f, BondType.Flag.HydrogenBond)) {
                 // show metall coordinations and hydrogen bonds with dashed cylinders
-                return LinkCylinderStyle.Dashed;
+                return LinkStyle.Dashed;
             } else if (o === 2) {
-                return LinkCylinderStyle.Double;
+                return LinkStyle.Double;
             } else if (o === 3) {
-                return LinkCylinderStyle.Triple;
+                return LinkStyle.Triple;
             } else {
-                return LinkCylinderStyle.Solid;
+                return LinkStyle.Solid;
             }
         },
         radius: (edgeIndex: number) => {
@@ -105,23 +102,23 @@ function createInterUnitBondCylinderMesh(ctx: VisualContext, structure: Structur
     return createLinkCylinderMesh(ctx, builderProps, props, mesh);
 }
 
-export const InterUnitBondParams = {
+export const InterUnitBondCylinderParams = {
     ...ComplexMeshParams,
     ...BondCylinderParams,
     sizeFactor: PD.Numeric(0.3, { min: 0, max: 10, step: 0.01 }),
     sizeAspectRatio: PD.Numeric(2 / 3, { min: 0, max: 3, step: 0.01 }),
     ignoreHydrogens: PD.Boolean(false),
 };
-export type InterUnitBondParams = typeof InterUnitBondParams
+export type InterUnitBondCylinderParams = typeof InterUnitBondCylinderParams
 
-export function InterUnitBondVisual(materialId: number): ComplexVisual<InterUnitBondParams> {
-    return ComplexMeshVisual<InterUnitBondParams>({
-        defaultProps: PD.getDefaultValues(InterUnitBondParams),
+export function InterUnitBondCylinderVisual(materialId: number): ComplexVisual<InterUnitBondCylinderParams> {
+    return ComplexMeshVisual<InterUnitBondCylinderParams>({
+        defaultProps: PD.getDefaultValues(InterUnitBondCylinderParams),
         createGeometry: createInterUnitBondCylinderMesh,
         createLocationIterator: BondIterator.fromStructure,
-        getLoci: getBondLoci,
-        eachLocation: eachBond,
-        setUpdateState: (state: VisualUpdateState, newProps: PD.Values<InterUnitBondParams>, currentProps: PD.Values<InterUnitBondParams>) => {
+        getLoci: getInterBondLoci,
+        eachLocation: eachInterBond,
+        setUpdateState: (state: VisualUpdateState, newProps: PD.Values<InterUnitBondCylinderParams>, currentProps: PD.Values<InterUnitBondCylinderParams>) => {
             state.createGeometry = (
                 newProps.sizeFactor !== currentProps.sizeFactor ||
                 newProps.sizeAspectRatio !== currentProps.sizeAspectRatio ||
@@ -136,60 +133,3 @@ export function InterUnitBondVisual(materialId: number): ComplexVisual<InterUnit
         }
     }, materialId);
 }
-
-function getBondLoci(pickingId: PickingId, structure: Structure, id: number) {
-    const { objectId, groupId } = pickingId;
-    if (id === objectId) {
-        const bond = structure.interUnitBonds.edges[groupId];
-        return Bond.Loci(structure, [
-            Bond.Location(
-                structure, bond.unitA, bond.indexA as StructureElement.UnitIndex,
-                structure, bond.unitB, bond.indexB as StructureElement.UnitIndex
-            ),
-            Bond.Location(
-                structure, bond.unitB, bond.indexB as StructureElement.UnitIndex,
-                structure, bond.unitA, bond.indexA as StructureElement.UnitIndex
-            )
-        ]);
-    }
-    return EmptyLoci;
-}
-
-function eachBond(loci: Loci, structure: Structure, apply: (interval: Interval) => boolean, isMarking: boolean) {
-    let changed = false;
-    if (Bond.isLoci(loci)) {
-        if (!Structure.areEquivalent(loci.structure, structure)) return false;
-        for (const b of loci.bonds) {
-            const idx = structure.interUnitBonds.getBondIndexFromLocation(b);
-            if (idx !== -1) {
-                if (apply(Interval.ofSingleton(idx))) changed = true;
-            }
-        }
-    } else if (StructureElement.Loci.is(loci)) {
-        if (!Structure.areEquivalent(loci.structure, structure)) return false;
-        if (loci.elements.length === 1) return false; // only a single unit
-
-        const map = new Map<number, OrderedSet<StructureElement.UnitIndex>>();
-        for (const e of loci.elements) map.set(e.unit.id, e.indices);
-
-        for (const e of loci.elements) {
-            const { unit } = e;
-            if (!Unit.isAtomic(unit)) continue;
-            structure.interUnitBonds.getConnectedUnits(unit).forEach(b => {
-                const otherLociIndices = map.get(b.unitB.id);
-                if (otherLociIndices) {
-                    OrderedSet.forEach(e.indices, v => {
-                        if (!b.connectedIndices.includes(v)) return;
-                        b.getEdges(v).forEach(bi => {
-                            if (!isMarking || OrderedSet.has(otherLociIndices, bi.indexB)) {
-                                const idx = structure.interUnitBonds.getEdgeIndex(v, unit, bi.indexB, b.unitB);
-                                if (apply(Interval.ofSingleton(idx))) changed = true;
-                            }
-                        });
-                    });
-                }
-            });
-        }
-    }
-    return changed;
-}

+ 135 - 0
src/mol-repr/structure/visual/bond-inter-unit-line.ts

@@ -0,0 +1,135 @@
+/**
+ * Copyright (c) 2020 mol* contributors, licensed under MIT, See LICENSE file for more info.
+ *
+ * @author Alexander Rose <alexander.rose@weirdbyte.de>
+ */
+
+import { ParamDefinition as PD } from '../../../mol-util/param-definition';
+import { VisualContext } from '../../visual';
+import { Structure, StructureElement, Bond, Unit } from '../../../mol-model/structure';
+import { Theme } from '../../../mol-theme/theme';
+import { Vec3 } from '../../../mol-math/linear-algebra';
+import { BitFlags, arrayEqual } from '../../../mol-util';
+import { LinkStyle, createLinkLines } from './util/link';
+import { ComplexVisual, ComplexLinesVisual, ComplexLinesParams } from '../complex-visual';
+import { VisualUpdateState } from '../../util';
+import { isHydrogen } from './util/common';
+import { BondType } from '../../../mol-model/structure/model/types';
+import { ignoreBondType, BondCylinderParams, BondIterator, getInterBondLoci, eachInterBond } from './util/bond';
+import { Lines } from '../../../mol-geo/geometry/lines/lines';
+
+const tmpRefPosBondIt = new Bond.ElementBondIterator();
+function setRefPosition(pos: Vec3, structure: Structure, unit: Unit.Atomic, index: StructureElement.UnitIndex) {
+    tmpRefPosBondIt.setElement(structure, unit, index);
+    while (tmpRefPosBondIt.hasNext) {
+        const bA = tmpRefPosBondIt.move();
+        bA.otherUnit.conformation.position(bA.otherUnit.elements[bA.otherIndex], pos);
+        return pos;
+    }
+    return null;
+}
+
+const tmpRef = Vec3();
+const tmpLoc = StructureElement.Location.create(void 0);
+
+function createInterUnitBondLines(ctx: VisualContext, structure: Structure, theme: Theme, props: PD.Values<InterUnitBondLineParams>, lines?: Lines) {
+    const bonds = structure.interUnitBonds;
+    const { edgeCount, edges } = bonds;
+    const { sizeFactor, sizeAspectRatio, ignoreHydrogens, includeTypes, excludeTypes } = props;
+
+    const include = BondType.fromNames(includeTypes);
+    const exclude = BondType.fromNames(excludeTypes);
+
+    const ignoreHydrogen = ignoreHydrogens ? (edgeIndex: number) => {
+        const b = edges[edgeIndex];
+        const uA = b.unitA, uB = b.unitB;
+        return isHydrogen(uA, uA.elements[b.indexA]) || isHydrogen(uB, uB.elements[b.indexB]);
+    } : () => false;
+
+    if (!edgeCount) return Lines.createEmpty(lines);
+
+    const builderProps = {
+        linkCount: edgeCount,
+        referencePosition: (edgeIndex: number) => {
+            const b = edges[edgeIndex];
+            let unitA: Unit, unitB: Unit;
+            let indexA: StructureElement.UnitIndex, indexB: StructureElement.UnitIndex;
+            if (b.unitA.id < b.unitB.id) {
+                unitA = b.unitA, unitB = b.unitB;
+                indexA = b.indexA, indexB = b.indexB;
+            } else if (b.unitA.id > b.unitB.id) {
+                unitA = b.unitB, unitB = b.unitA;
+                indexA = b.indexB, indexB = b.indexA;
+            } else {
+                throw new Error('same units in createInterUnitBondCylinderMesh');
+            }
+            return setRefPosition(tmpRef, structure, unitA, indexA) || setRefPosition(tmpRef, structure, unitB, indexB);
+        },
+        position: (posA: Vec3, posB: Vec3, edgeIndex: number) => {
+            const b = edges[edgeIndex];
+            const uA = b.unitA, uB = b.unitB;
+            uA.conformation.position(uA.elements[b.indexA], posA);
+            uB.conformation.position(uB.elements[b.indexB], posB);
+        },
+        style: (edgeIndex: number) => {
+            const o = edges[edgeIndex].props.order;
+            const f = BitFlags.create(edges[edgeIndex].props.flag);
+            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;
+            }
+        },
+        radius: (edgeIndex: number) => {
+            const b = edges[edgeIndex];
+            tmpLoc.structure = structure;
+            tmpLoc.unit = b.unitA;
+            tmpLoc.element = b.unitA.elements[b.indexA];
+            const sizeA = theme.size.size(tmpLoc);
+            tmpLoc.unit = b.unitB;
+            tmpLoc.element = b.unitB.elements[b.indexB];
+            const sizeB = theme.size.size(tmpLoc);
+            return Math.min(sizeA, sizeB) * sizeFactor * sizeAspectRatio;
+        },
+        ignore: (edgeIndex: number) => ignoreHydrogen(edgeIndex) || ignoreBondType(include, exclude, edges[edgeIndex].props.flag)
+    };
+
+    return createLinkLines(ctx, builderProps, props, lines);
+}
+
+export const InterUnitBondLineParams = {
+    ...ComplexLinesParams,
+    ...BondCylinderParams,
+    sizeFactor: PD.Numeric(0.3, { min: 0, max: 10, step: 0.01 }),
+    sizeAspectRatio: PD.Numeric(2 / 3, { min: 0, max: 3, step: 0.01 }),
+    ignoreHydrogens: PD.Boolean(false),
+};
+export type InterUnitBondLineParams = typeof InterUnitBondLineParams
+
+export function InterUnitBondLineVisual(materialId: number): ComplexVisual<InterUnitBondLineParams> {
+    return ComplexLinesVisual<InterUnitBondLineParams>({
+        defaultProps: PD.getDefaultValues(InterUnitBondLineParams),
+        createGeometry: createInterUnitBondLines,
+        createLocationIterator: BondIterator.fromStructure,
+        getLoci: getInterBondLoci,
+        eachLocation: eachInterBond,
+        setUpdateState: (state: VisualUpdateState, newProps: PD.Values<InterUnitBondLineParams>, currentProps: PD.Values<InterUnitBondLineParams>) => {
+            state.createGeometry = (
+                newProps.sizeFactor !== currentProps.sizeFactor ||
+                newProps.sizeAspectRatio !== currentProps.sizeAspectRatio ||
+                newProps.radialSegments !== currentProps.radialSegments ||
+                newProps.linkScale !== currentProps.linkScale ||
+                newProps.linkSpacing !== currentProps.linkSpacing ||
+                newProps.ignoreHydrogens !== currentProps.ignoreHydrogens ||
+                newProps.linkCap !== currentProps.linkCap ||
+                !arrayEqual(newProps.includeTypes, currentProps.includeTypes) ||
+                !arrayEqual(newProps.excludeTypes, currentProps.excludeTypes)
+            );
+        }
+    }, materialId);
+}

+ 17 - 81
src/mol-repr/structure/visual/bond-intra-unit-cylinder.ts

@@ -7,23 +7,20 @@
 
 import { ParamDefinition as PD } from '../../../mol-util/param-definition';
 import { VisualContext } from '../../visual';
-import { Unit, Structure, StructureElement, Bond } from '../../../mol-model/structure';
+import { Unit, Structure, StructureElement } from '../../../mol-model/structure';
 import { Theme } from '../../../mol-theme/theme';
 import { Mesh } from '../../../mol-geo/geometry/mesh/mesh';
 import { Vec3 } from '../../../mol-math/linear-algebra';
 import { BitFlags, arrayEqual } from '../../../mol-util';
-import { createLinkCylinderMesh, LinkCylinderStyle } from './util/link';
-import { UnitsMeshParams, UnitsVisual, UnitsMeshVisual, StructureGroup } from '../units-visual';
+import { createLinkCylinderMesh, LinkStyle } from './util/link';
+import { UnitsMeshParams, UnitsVisual, UnitsMeshVisual } from '../units-visual';
 import { VisualUpdateState } from '../../util';
-import { PickingId } from '../../../mol-geo/geometry/picking';
-import { EmptyLoci, Loci } from '../../../mol-model/loci';
-import { Interval, OrderedSet } from '../../../mol-data/int';
 import { isHydrogen } from './util/common';
 import { BondType } from '../../../mol-model/structure/model/types';
-import { ignoreBondType, BondCylinderParams, BondIterator } from './util/bond';
+import { ignoreBondType, BondCylinderParams, BondIterator, eachIntraBond, getIntraBondLoci } from './util/bond';
 import { Sphere3D } from '../../../mol-math/geometry';
 
-function createIntraUnitBondCylinderMesh(ctx: VisualContext, unit: Unit, structure: Structure, theme: Theme, props: PD.Values<IntraUnitBondParams>, mesh?: Mesh) {
+function createIntraUnitBondCylinderMesh(ctx: VisualContext, unit: Unit, structure: Structure, theme: Theme, props: PD.Values<IntraUnitBondCylinderParams>, mesh?: Mesh) {
     if (!Unit.isAtomic(unit)) return Mesh.createEmpty(mesh);
 
     const location = StructureElement.Location.create(structure, unit);
@@ -74,13 +71,13 @@ function createIntraUnitBondCylinderMesh(ctx: VisualContext, unit: Unit, structu
             const f = BitFlags.create(_flags[edgeIndex]);
             if (BondType.is(f, BondType.Flag.MetallicCoordination) || BondType.is(f, BondType.Flag.HydrogenBond)) {
                 // show metall coordinations and hydrogen bonds with dashed cylinders
-                return LinkCylinderStyle.Dashed;
+                return LinkStyle.Dashed;
             } else if (o === 2) {
-                return LinkCylinderStyle.Double;
+                return LinkStyle.Double;
             } else if (o === 3) {
-                return LinkCylinderStyle.Triple;
+                return LinkStyle.Triple;
             } else {
-                return LinkCylinderStyle.Solid;
+                return LinkStyle.Solid;
             }
         },
         radius: (edgeIndex: number) => {
@@ -101,23 +98,23 @@ function createIntraUnitBondCylinderMesh(ctx: VisualContext, unit: Unit, structu
     return m;
 }
 
-export const IntraUnitBondParams = {
+export const IntraUnitBondCylinderParams = {
     ...UnitsMeshParams,
     ...BondCylinderParams,
     sizeFactor: PD.Numeric(0.3, { min: 0, max: 10, step: 0.01 }),
     sizeAspectRatio: PD.Numeric(2 / 3, { min: 0, max: 3, step: 0.01 }),
     ignoreHydrogens: PD.Boolean(false),
 };
-export type IntraUnitBondParams = typeof IntraUnitBondParams
+export type IntraUnitBondCylinderParams = typeof IntraUnitBondCylinderParams
 
-export function IntraUnitBondVisual(materialId: number): UnitsVisual<IntraUnitBondParams> {
-    return UnitsMeshVisual<IntraUnitBondParams>({
-        defaultProps: PD.getDefaultValues(IntraUnitBondParams),
+export function IntraUnitBondCylinderVisual(materialId: number): UnitsVisual<IntraUnitBondCylinderParams> {
+    return UnitsMeshVisual<IntraUnitBondCylinderParams>({
+        defaultProps: PD.getDefaultValues(IntraUnitBondCylinderParams),
         createGeometry: createIntraUnitBondCylinderMesh,
         createLocationIterator: BondIterator.fromGroup,
-        getLoci: getBondLoci,
-        eachLocation: eachBond,
-        setUpdateState: (state: VisualUpdateState, newProps: PD.Values<IntraUnitBondParams>, currentProps: PD.Values<IntraUnitBondParams>) => {
+        getLoci: getIntraBondLoci,
+        eachLocation: eachIntraBond,
+        setUpdateState: (state: VisualUpdateState, newProps: PD.Values<IntraUnitBondCylinderParams>, currentProps: PD.Values<IntraUnitBondCylinderParams>) => {
             state.createGeometry = (
                 newProps.sizeFactor !== currentProps.sizeFactor ||
                 newProps.sizeAspectRatio !== currentProps.sizeAspectRatio ||
@@ -131,65 +128,4 @@ export function IntraUnitBondVisual(materialId: number): UnitsVisual<IntraUnitBo
             );
         }
     }, materialId);
-}
-
-function getBondLoci(pickingId: PickingId, structureGroup: StructureGroup, id: number) {
-    const { objectId, instanceId, groupId } = pickingId;
-    if (id === objectId) {
-        const { structure, group } = structureGroup;
-        const unit = group.units[instanceId];
-        if (Unit.isAtomic(unit)) {
-            return Bond.Loci(structure, [
-                Bond.Location(
-                    structure, unit, unit.bonds.a[groupId] as StructureElement.UnitIndex,
-                    structure, unit, unit.bonds.b[groupId] as StructureElement.UnitIndex
-                ),
-                Bond.Location(
-                    structure, unit, unit.bonds.b[groupId] as StructureElement.UnitIndex,
-                    structure, unit, unit.bonds.a[groupId] as StructureElement.UnitIndex
-                )
-            ]);
-        }
-    }
-    return EmptyLoci;
-}
-
-function eachBond(loci: Loci, structureGroup: StructureGroup, apply: (interval: Interval) => boolean, isMarking: boolean) {
-    let changed = false;
-    if (Bond.isLoci(loci)) {
-        const { structure, group } = structureGroup;
-        if (!Structure.areEquivalent(loci.structure, structure)) return false;
-        const unit = group.units[0];
-        if (!Unit.isAtomic(unit)) return false;
-        const groupCount = unit.bonds.edgeCount * 2;
-        for (const b of loci.bonds) {
-            const unitIdx = group.unitIndexMap.get(b.aUnit.id);
-            if (unitIdx !== undefined) {
-                const idx = unit.bonds.getDirectedEdgeIndex(b.aIndex, b.bIndex);
-                if (idx !== -1) {
-                    if (apply(Interval.ofSingleton(unitIdx * groupCount + idx))) changed = true;
-                }
-            }
-        }
-    } else if (StructureElement.Loci.is(loci)) {
-        const { structure, group } = structureGroup;
-        if (!Structure.areEquivalent(loci.structure, structure)) return false;
-        const unit = group.units[0];
-        if (!Unit.isAtomic(unit)) return false;
-        const groupCount = unit.bonds.edgeCount * 2;
-        for (const e of loci.elements) {
-            const unitIdx = group.unitIndexMap.get(e.unit.id);
-            if (unitIdx !== undefined) {
-                const { offset, b } = unit.bonds;
-                OrderedSet.forEach(e.indices, v => {
-                    for (let t = offset[v], _t = offset[v + 1]; t < _t; t++) {
-                        if (!isMarking || OrderedSet.has(e.indices, b[t])) {
-                            if (apply(Interval.ofSingleton(unitIdx * groupCount + t))) changed = true;
-                        }
-                    }
-                });
-            }
-        }
-    }
-    return changed;
 }

+ 125 - 0
src/mol-repr/structure/visual/bond-intra-unit-line.ts

@@ -0,0 +1,125 @@
+/**
+ * Copyright (c) 2020 mol* contributors, licensed under MIT, See LICENSE file for more info.
+ *
+ * @author Alexander Rose <alexander.rose@weirdbyte.de>
+ */
+
+import { ParamDefinition as PD } from '../../../mol-util/param-definition';
+import { VisualContext } from '../../visual';
+import { Unit, Structure, StructureElement } from '../../../mol-model/structure';
+import { Theme } from '../../../mol-theme/theme';
+import { Vec3 } from '../../../mol-math/linear-algebra';
+import { BitFlags, arrayEqual } from '../../../mol-util';
+import { LinkStyle, createLinkLines } from './util/link';
+import { UnitsVisual, UnitsLinesParams, UnitsLinesVisual } from '../units-visual';
+import { VisualUpdateState } from '../../util';
+import { isHydrogen } from './util/common';
+import { BondType } from '../../../mol-model/structure/model/types';
+import { ignoreBondType, BondIterator, BondLineParams, getIntraBondLoci, eachIntraBond } from './util/bond';
+import { Sphere3D } from '../../../mol-math/geometry';
+import { Lines } from '../../../mol-geo/geometry/lines/lines';
+
+function createIntraUnitBondLines(ctx: VisualContext, unit: Unit, structure: Structure, theme: Theme, props: PD.Values<IntraUnitBondLineParams>, lines?: Lines) {
+    if (!Unit.isAtomic(unit)) return Lines.createEmpty(lines);
+
+    const location = StructureElement.Location.create(structure, unit);
+
+    const elements = unit.elements;
+    const bonds = unit.bonds;
+    const { edgeCount, a, b, edgeProps, offset } = bonds;
+    const { order: _order, flags: _flags } = edgeProps;
+    const { sizeFactor, ignoreHydrogens, includeTypes, excludeTypes } = props;
+
+    const include = BondType.fromNames(includeTypes);
+    const exclude = BondType.fromNames(excludeTypes);
+
+    const ignoreHydrogen = ignoreHydrogens ? (edgeIndex: number) => {
+        return isHydrogen(unit, elements[a[edgeIndex]]) || isHydrogen(unit, elements[b[edgeIndex]]);
+    } : () => false;
+
+    if (!edgeCount) return Lines.createEmpty(lines);
+
+    const vRef = Vec3.zero();
+    const pos = unit.conformation.invariantPosition;
+
+    const builderProps = {
+        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 in rings
+
+            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);
+            }
+            return null;
+        },
+        position: (posA: Vec3, posB: Vec3, edgeIndex: number) => {
+            pos(elements[a[edgeIndex]], posA);
+            pos(elements[b[edgeIndex]], posB);
+        },
+        style: (edgeIndex: number) => {
+            const o = _order[edgeIndex];
+            const f = BitFlags.create(_flags[edgeIndex]);
+            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;
+            }
+        },
+        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;
+        },
+        ignore: (edgeIndex: number) => ignoreHydrogen(edgeIndex) || ignoreBondType(include, exclude, _flags[edgeIndex])
+    };
+
+    const l = createLinkLines(ctx, builderProps, props, lines);
+
+    const sphere = Sphere3D.expand(Sphere3D(), unit.boundary.sphere, 1 * sizeFactor);
+    l.setBoundingSphere(sphere);
+
+    return l;
+}
+
+export const IntraUnitBondLineParams = {
+    ...UnitsLinesParams,
+    ...BondLineParams,
+    ignoreHydrogens: PD.Boolean(false),
+};
+export type IntraUnitBondLineParams = typeof IntraUnitBondLineParams
+
+export function IntraUnitBondLineVisual(materialId: number): UnitsVisual<IntraUnitBondLineParams> {
+    return UnitsLinesVisual<IntraUnitBondLineParams>({
+        defaultProps: PD.getDefaultValues(IntraUnitBondLineParams),
+        createGeometry: createIntraUnitBondLines,
+        createLocationIterator: BondIterator.fromGroup,
+        getLoci: getIntraBondLoci,
+        eachLocation: eachIntraBond,
+        setUpdateState: (state: VisualUpdateState, newProps: PD.Values<IntraUnitBondLineParams>, currentProps: PD.Values<IntraUnitBondLineParams>) => {
+            state.createGeometry = (
+                newProps.sizeFactor !== currentProps.sizeFactor ||
+                newProps.linkScale !== currentProps.linkScale ||
+                newProps.linkSpacing !== currentProps.linkSpacing ||
+                newProps.ignoreHydrogens !== currentProps.ignoreHydrogens ||
+                !arrayEqual(newProps.includeTypes, currentProps.includeTypes) ||
+                !arrayEqual(newProps.excludeTypes, currentProps.excludeTypes)
+            );
+        }
+    }, materialId);
+}

+ 2 - 2
src/mol-repr/structure/visual/carbohydrate-terminal-link-cylinder.ts

@@ -10,7 +10,7 @@ import { Structure, StructureElement, Unit } from '../../../mol-model/structure'
 import { Theme } from '../../../mol-theme/theme';
 import { Mesh } from '../../../mol-geo/geometry/mesh/mesh';
 import { Vec3 } from '../../../mol-math/linear-algebra';
-import { createLinkCylinderMesh, LinkCylinderParams, LinkCylinderStyle } from './util/link';
+import { createLinkCylinderMesh, LinkCylinderParams, LinkStyle } from './util/link';
 import { UnitsMeshParams } from '../units-visual';
 import { ComplexVisual, ComplexMeshVisual } from '../complex-visual';
 import { VisualUpdateState } from '../../util';
@@ -56,7 +56,7 @@ function createCarbohydrateTerminalLinkCylinderMesh(ctx: VisualContext, structur
             const l = terminalLinks[edgeIndex];
             const eI = l.elementUnit.elements[l.elementIndex];
             const beI = getElementIdx(l.elementUnit.model.atomicHierarchy.atoms.type_symbol.value(eI));
-            return MetalsSet.has(beI) ? LinkCylinderStyle.Dashed : LinkCylinderStyle.Solid;
+            return MetalsSet.has(beI) ? LinkStyle.Dashed : LinkStyle.Solid;
         }
     };
 

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

@@ -5,21 +5,37 @@
  */
 
 import { BondType } from '../../../../mol-model/structure/model/types';
-import { Unit, StructureElement, Structure } from '../../../../mol-model/structure';
+import { Unit, StructureElement, Structure, Bond } from '../../../../mol-model/structure';
 import { ParamDefinition as PD } from '../../../../mol-util/param-definition';
 import { LocationIterator } from '../../../../mol-geo/util/location-iterator';
 import { StructureGroup } from '../../units-visual';
-import { LinkCylinderParams } from './link';
+import { LinkCylinderParams, LinkParams } from './link';
 import { ObjectKeys } from '../../../../mol-util/type-helpers';
+import { PickingId } from '../../../../mol-geo/geometry/picking';
+import { EmptyLoci, Loci } from '../../../../mol-model/loci';
+import { Interval, OrderedSet } from '../../../../mol-data/int';
 
-export const BondCylinderParams = {
-    ...LinkCylinderParams,
+export const BondParams = {
     includeTypes: PD.MultiSelect(ObjectKeys(BondType.Names), PD.objectToOptions(BondType.Names)),
     excludeTypes: PD.MultiSelect([] as BondType.Names[], PD.objectToOptions(BondType.Names)),
 };
+export const DefaultBondProps = PD.getDefaultValues(BondParams);
+export type BondProps = typeof DefaultBondProps
+
+export const BondCylinderParams = {
+    ...LinkCylinderParams,
+    ...BondParams
+};
 export const DefaultBondCylinderProps = PD.getDefaultValues(BondCylinderParams);
 export type BondCylinderProps = typeof DefaultBondCylinderProps
 
+export const BondLineParams = {
+    ...LinkParams,
+    ...BondParams
+};
+export const DefaultBondLineProps = PD.getDefaultValues(BondLineParams);
+export type BondLineProps = typeof DefaultBondLineProps
+
 export function ignoreBondType(include: BondType.Flag, exclude: BondType.Flag, f: BondType.Flag) {
     return !BondType.is(include, f) || BondType.is(exclude, f);
 }
@@ -52,4 +68,126 @@ export namespace BondIterator {
         };
         return LocationIterator(groupCount, instanceCount, getLocation, true);
     }
+}
+
+//
+
+export function getIntraBondLoci(pickingId: PickingId, structureGroup: StructureGroup, id: number) {
+    const { objectId, instanceId, groupId } = pickingId;
+    if (id === objectId) {
+        const { structure, group } = structureGroup;
+        const unit = group.units[instanceId];
+        if (Unit.isAtomic(unit)) {
+            return Bond.Loci(structure, [
+                Bond.Location(
+                    structure, unit, unit.bonds.a[groupId] as StructureElement.UnitIndex,
+                    structure, unit, unit.bonds.b[groupId] as StructureElement.UnitIndex
+                ),
+                Bond.Location(
+                    structure, unit, unit.bonds.b[groupId] as StructureElement.UnitIndex,
+                    structure, unit, unit.bonds.a[groupId] as StructureElement.UnitIndex
+                )
+            ]);
+        }
+    }
+    return EmptyLoci;
+}
+
+export function eachIntraBond(loci: Loci, structureGroup: StructureGroup, apply: (interval: Interval) => boolean, isMarking: boolean) {
+    let changed = false;
+    if (Bond.isLoci(loci)) {
+        const { structure, group } = structureGroup;
+        if (!Structure.areEquivalent(loci.structure, structure)) return false;
+        const unit = group.units[0];
+        if (!Unit.isAtomic(unit)) return false;
+        const groupCount = unit.bonds.edgeCount * 2;
+        for (const b of loci.bonds) {
+            const unitIdx = group.unitIndexMap.get(b.aUnit.id);
+            if (unitIdx !== undefined) {
+                const idx = unit.bonds.getDirectedEdgeIndex(b.aIndex, b.bIndex);
+                if (idx !== -1) {
+                    if (apply(Interval.ofSingleton(unitIdx * groupCount + idx))) changed = true;
+                }
+            }
+        }
+    } else if (StructureElement.Loci.is(loci)) {
+        const { structure, group } = structureGroup;
+        if (!Structure.areEquivalent(loci.structure, structure)) return false;
+        const unit = group.units[0];
+        if (!Unit.isAtomic(unit)) return false;
+        const groupCount = unit.bonds.edgeCount * 2;
+        for (const e of loci.elements) {
+            const unitIdx = group.unitIndexMap.get(e.unit.id);
+            if (unitIdx !== undefined) {
+                const { offset, b } = unit.bonds;
+                OrderedSet.forEach(e.indices, v => {
+                    for (let t = offset[v], _t = offset[v + 1]; t < _t; t++) {
+                        if (!isMarking || OrderedSet.has(e.indices, b[t])) {
+                            if (apply(Interval.ofSingleton(unitIdx * groupCount + t))) changed = true;
+                        }
+                    }
+                });
+            }
+        }
+    }
+    return changed;
+}
+
+//
+
+export function getInterBondLoci(pickingId: PickingId, structure: Structure, id: number) {
+    const { objectId, groupId } = pickingId;
+    if (id === objectId) {
+        const bond = structure.interUnitBonds.edges[groupId];
+        return Bond.Loci(structure, [
+            Bond.Location(
+                structure, bond.unitA, bond.indexA as StructureElement.UnitIndex,
+                structure, bond.unitB, bond.indexB as StructureElement.UnitIndex
+            ),
+            Bond.Location(
+                structure, bond.unitB, bond.indexB as StructureElement.UnitIndex,
+                structure, bond.unitA, bond.indexA as StructureElement.UnitIndex
+            )
+        ]);
+    }
+    return EmptyLoci;
+}
+
+export function eachInterBond(loci: Loci, structure: Structure, apply: (interval: Interval) => boolean, isMarking: boolean) {
+    let changed = false;
+    if (Bond.isLoci(loci)) {
+        if (!Structure.areEquivalent(loci.structure, structure)) return false;
+        for (const b of loci.bonds) {
+            const idx = structure.interUnitBonds.getBondIndexFromLocation(b);
+            if (idx !== -1) {
+                if (apply(Interval.ofSingleton(idx))) changed = true;
+            }
+        }
+    } else if (StructureElement.Loci.is(loci)) {
+        if (!Structure.areEquivalent(loci.structure, structure)) return false;
+        if (loci.elements.length === 1) return false; // only a single unit
+
+        const map = new Map<number, OrderedSet<StructureElement.UnitIndex>>();
+        for (const e of loci.elements) map.set(e.unit.id, e.indices);
+
+        for (const e of loci.elements) {
+            const { unit } = e;
+            if (!Unit.isAtomic(unit)) continue;
+            structure.interUnitBonds.getConnectedUnits(unit).forEach(b => {
+                const otherLociIndices = map.get(b.unitB.id);
+                if (otherLociIndices) {
+                    OrderedSet.forEach(e.indices, v => {
+                        if (!b.connectedIndices.includes(v)) return;
+                        b.getEdges(v).forEach(bi => {
+                            if (!isMarking || OrderedSet.has(otherLociIndices, bi.indexB)) {
+                                const idx = structure.interUnitBonds.getEdgeIndex(v, unit, bi.indexB, b.unitB);
+                                if (apply(Interval.ofSingleton(idx))) changed = true;
+                            }
+                        });
+                    });
+                }
+            });
+        }
+    }
+    return changed;
 }

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

@@ -12,10 +12,18 @@ import { CylinderProps } from '../../../../mol-geo/primitive/cylinder';
 import { addFixedCountDashedCylinder, addCylinder, addDoubleCylinder } from '../../../../mol-geo/geometry/mesh/builder/cylinder';
 import { VisualContext } from '../../../visual';
 import { BaseGeometry } from '../../../../mol-geo/geometry/base';
+import { Lines } from '../../../../mol-geo/geometry/lines/lines';
+import { LinesBuilder } from '../../../../mol-geo/geometry/lines/lines-builder';
 
-export const LinkCylinderParams = {
+export const LinkParams = {
     linkScale: PD.Numeric(0.4, { min: 0, max: 1, step: 0.1 }),
     linkSpacing: PD.Numeric(1, { min: 0, max: 2, step: 0.01 }),
+};
+export const DefaultLinkProps = PD.getDefaultValues(LinkParams);
+export type LinkProps = typeof DefaultLinkProps
+
+export const LinkCylinderParams = {
+    ...LinkParams,
     linkCap: PD.Boolean(false),
     radialSegments: PD.Numeric(16, { min: 2, max: 56, step: 2 }, BaseGeometry.CustomQualityParamInfo),
 };
@@ -53,16 +61,16 @@ export function calculateShiftDir (out: Vec3, v1: Vec3, v2: Vec3, v3: Vec3 | nul
     return Vec3.normalize(out, tmpShiftV13);
 }
 
-export interface LinkCylinderMeshBuilderProps {
+export interface LinkBuilderProps {
     linkCount: number
     position: (posA: Vec3, posB: Vec3, edgeIndex: number) => void
     radius: (edgeIndex: number) => number
     referencePosition?: (edgeIndex: number) => Vec3 | null
-    style?: (edgeIndex: number) => LinkCylinderStyle
+    style?: (edgeIndex: number) => LinkStyle
     ignore?: (edgeIndex: number) => boolean
 }
 
-export enum LinkCylinderStyle {
+export enum LinkStyle {
     Solid = 0,
     Dashed = 1,
     Double = 2,
@@ -74,7 +82,7 @@ export enum LinkCylinderStyle {
  * Each edge is included twice to allow for coloring/picking
  * the half closer to the first vertex, i.e. vertex a.
  */
-export function createLinkCylinderMesh(ctx: VisualContext, linkBuilder: LinkCylinderMeshBuilderProps, props: LinkCylinderProps, mesh?: Mesh) {
+export function createLinkCylinderMesh(ctx: VisualContext, linkBuilder: LinkBuilderProps, props: LinkCylinderProps, mesh?: Mesh) {
     const { linkCount, referencePosition, position, style, radius, ignore } = linkBuilder;
 
     if (!linkCount) return Mesh.createEmpty(mesh);
@@ -84,9 +92,9 @@ export function createLinkCylinderMesh(ctx: VisualContext, linkBuilder: LinkCyli
     const vertexCountEstimate = radialSegments * 2 * linkCount * 2;
     const builderState = MeshBuilder.createState(vertexCountEstimate, vertexCountEstimate / 4, mesh);
 
-    const va = Vec3.zero();
-    const vb = Vec3.zero();
-    const vShift = Vec3.zero();
+    const va = Vec3();
+    const vb = Vec3();
+    const vShift = Vec3();
     const cylinderProps: CylinderProps = {
         radiusTop: 1,
         radiusBottom: 1,
@@ -101,16 +109,16 @@ export function createLinkCylinderMesh(ctx: VisualContext, linkBuilder: LinkCyli
         position(va, vb, edgeIndex);
 
         const linkRadius = radius(edgeIndex);
-        const linkStyle = style ? style(edgeIndex) : LinkCylinderStyle.Solid;
+        const linkStyle = style ? style(edgeIndex) : LinkStyle.Solid;
         builderState.currentGroup = edgeIndex;
 
-        if (linkStyle === LinkCylinderStyle.Dashed) {
+        if (linkStyle === LinkStyle.Dashed) {
             cylinderProps.radiusTop = cylinderProps.radiusBottom = linkRadius / 3;
             cylinderProps.topCap = cylinderProps.bottomCap = true;
 
             addFixedCountDashedCylinder(builderState, va, vb, 0.5, 7, cylinderProps);
-        } else if (linkStyle === LinkCylinderStyle.Double || linkStyle === LinkCylinderStyle.Triple) {
-            const order = LinkCylinderStyle.Double ? 2 : 3;
+        } else if (linkStyle === LinkStyle.Double || linkStyle === LinkStyle.Triple) {
+            const order = LinkStyle.Double ? 2 : 3;
             const multiRadius = linkRadius * (linkScale / (0.5 * order));
             const absOffset = (linkRadius - multiRadius) * linkSpacing;
 
@@ -122,7 +130,7 @@ export function createLinkCylinderMesh(ctx: VisualContext, linkBuilder: LinkCyli
 
             if (order === 3) addCylinder(builderState, va, vb, 0.5, cylinderProps);
             addDoubleCylinder(builderState, va, vb, 0.5, vShift, cylinderProps);
-        } else if (linkStyle === LinkCylinderStyle.Disk) {
+        } else if (linkStyle === LinkStyle.Disk) {
             Vec3.scale(tmpV12, Vec3.sub(tmpV12, vb, va), 0.475);
             Vec3.add(va, va, tmpV12);
             Vec3.sub(vb, vb, tmpV12);
@@ -146,4 +154,60 @@ export function createLinkCylinderMesh(ctx: VisualContext, linkBuilder: LinkCyli
     }
 
     return MeshBuilder.getMesh(builderState);
+}
+
+/**
+ * Each edge is included twice to allow for coloring/picking
+ * the half closer to the first vertex, i.e. vertex a.
+ */
+export function createLinkLines(ctx: VisualContext, linkBuilder: LinkBuilderProps, props: LinkProps, lines?: Lines) {
+    const { linkCount, referencePosition, position, style, radius, ignore } = linkBuilder;
+
+    if (!linkCount) return Lines.createEmpty(lines);
+
+    const { linkScale, linkSpacing } = props;
+
+    const linesCountEstimate = linkCount * 2;
+    const builder = LinesBuilder.create(linesCountEstimate, linesCountEstimate / 4, lines);
+
+    const va = Vec3();
+    const vb = Vec3();
+    const vShift = Vec3();
+
+    for (let edgeIndex = 0, _eI = linkCount; edgeIndex < _eI; ++edgeIndex) {
+        if (ignore && ignore(edgeIndex)) continue;
+
+        position(va, vb, edgeIndex);
+        Vec3.scale(vb, Vec3.add(vb, va, vb), 0.5);
+
+        // TODO use line width?
+        const linkRadius = radius(edgeIndex);
+        const linkStyle = style ? style(edgeIndex) : LinkStyle.Solid;
+
+        if (linkStyle === LinkStyle.Dashed) {
+            builder.addFixedCountDashes(va, vb, 7, edgeIndex);
+        } else if (linkStyle === LinkStyle.Double || linkStyle === LinkStyle.Triple) {
+            const order = LinkStyle.Double ? 2 : 3;
+            const multiRadius = linkRadius * (linkScale / (0.5 * order));
+            const absOffset = (linkRadius - multiRadius) * linkSpacing;
+
+            calculateShiftDir(vShift, va, vb, referencePosition ? referencePosition(edgeIndex) : null);
+            Vec3.setMagnitude(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);
+        } else if (linkStyle === LinkStyle.Disk) {
+            Vec3.scale(tmpV12, Vec3.sub(tmpV12, vb, va), 0.475);
+            Vec3.add(va, va, tmpV12);
+            Vec3.sub(vb, vb, tmpV12);
+
+            // TODO what to do here?
+            builder.add(va[0], va[1], va[2], vb[0], vb[1], vb[2], edgeIndex);
+        } else {
+            builder.add(va[0], va[1], va[2], vb[0], vb[1], vb[2], edgeIndex);
+        }
+    }
+
+    return builder.getLines();
 }