Browse Source

Merge branch 'master' of https://github.com/molstar/molstar-proto into plugin

David Sehnal 6 years ago
parent
commit
f1bb6f28d1

+ 12 - 10
src/mol-canvas3d/canvas3d.ts

@@ -244,7 +244,7 @@ namespace Canvas3D {
         }
 
         async function identify(x: number, y: number): Promise<PickingId | undefined> {
-            if (pickDirty) return undefined
+            if (pickDirty || isPicking) return undefined
 
             isPicking = true
 
@@ -257,25 +257,27 @@ namespace Canvas3D {
             const yp = Math.round(y * pickScale)
 
             objectPickTarget.bind()
-            await webgl.readPixelsAsync(xp, yp, 1, 1, buffer)
+            // TODO slow in Chrome, ok in FF; doesn't play well with gpu surface calc
+            // await webgl.readPixelsAsync(xp, yp, 1, 1, buffer)
+            webgl.readPixels(xp, yp, 1, 1, buffer)
             const objectId = decodeIdRGB(buffer[0], buffer[1], buffer[2])
+            if (objectId === -1) return
 
             instancePickTarget.bind()
-            await webgl.readPixels(xp, yp, 1, 1, buffer)
+            // await webgl.readPixelsAsync(xp, yp, 1, 1, buffer)
+            webgl.readPixels(xp, yp, 1, 1, buffer)
             const instanceId = decodeIdRGB(buffer[0], buffer[1], buffer[2])
+            if (instanceId === -1) return
 
             groupPickTarget.bind()
-            await webgl.readPixels(xp, yp, 1, 1, buffer)
+            // await webgl.readPixelsAsync(xp, yp, 1, 1, buffer)
+            webgl.readPixels(xp, yp, 1, 1, buffer)
             const groupId = decodeIdRGB(buffer[0], buffer[1], buffer[2])
+            if (groupId === -1) return
 
             isPicking = false
 
-            // TODO
-            if (objectId === -1 || instanceId === -1 || groupId === -1) {
-                return { objectId: -1, instanceId: -1, groupId: -1 }
-            } else {
-                return { objectId, instanceId, groupId }
-            }
+            return { objectId, instanceId, groupId }
         }
 
         function add(repr: Representation.Any) {

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

@@ -62,7 +62,6 @@ export namespace Geometry {
 
     export const Params = {
         alpha: PD.Numeric(1, { min: 0, max: 1, step: 0.01 }, { label: 'Opacity' }),
-        depthMask: PD.Boolean(true),
         useFog: PD.Boolean(false),
         highlightColor: PD.Color(Color.fromNormalizedRgb(1.0, 0.4, 0.6)),
         selectColor: PD.Color(Color.fromNormalizedRgb(0.2, 1.0, 0.1)),
@@ -105,12 +104,11 @@ export function createRenderableState(props: PD.Values<Geometry.Params>): Render
     return {
         visible: true,
         pickable: true,
-        depthMask: props.depthMask
     }
 }
 
 export function updateRenderableState(state: RenderableState, props: PD.Values<Geometry.Params>) {
-    state.depthMask = props.depthMask
+    
 }
 
 //

+ 0 - 1
src/mol-gl/_spec/renderer.spec.ts

@@ -86,7 +86,6 @@ function createPoints() {
     const state: RenderableState = {
         visible: true,
         pickable: true,
-        depthMask: true,
     }
 
     return createPointsRenderObject(values, state)

+ 0 - 1
src/mol-gl/renderable.ts

@@ -14,7 +14,6 @@ import { ValueCell } from 'mol-util';
 export type RenderableState = {
     visible: boolean
     pickable: boolean
-    depthMask: boolean
 }
 
 export interface Renderable<T extends RenderableValues> {

+ 2 - 0
src/mol-gl/renderable/direct-volume.ts

@@ -35,6 +35,8 @@ export const DirectVolumeSchema = {
     elements: ElementsSpec('uint32'),
 
     uAlpha: UniformSpec('f'),
+    uHighlightColor: UniformSpec('v3'),
+    uSelectColor: UniformSpec('v3'),
     dUseFog: DefineSpec('boolean'),
 
     uIsoValue: UniformSpec('f'),

+ 1 - 1
src/mol-gl/renderer.ts

@@ -139,7 +139,7 @@ namespace Renderer {
                     gl.cullFace(gl.BACK)
                 }
 
-                gl.depthMask(r.state.depthMask)
+                gl.depthMask(r.opaque)
 
                 r.render(variant)
             }

+ 7 - 2
src/mol-gl/shader/direct-volume.frag

@@ -11,7 +11,6 @@ varying vec3 unitCoord;
 varying vec3 origPos;
 varying float instance;
 
-uniform float uAlpha;
 uniform mat4 uInvView;
 uniform float uIsoValue;
 uniform vec3 uGridDim;
@@ -26,6 +25,10 @@ uniform vec3 uSelectColor;
 uniform vec2 uMarkerTexDim;
 uniform sampler2D tMarker;
 
+uniform float uAlpha;
+uniform float uPickingAlphaThreshold;
+uniform int uPickable;
+
 #if defined(dGridTexType_2d)
     precision mediump sampler2D;
     uniform sampler2D tGridTex;
@@ -126,7 +129,9 @@ vec4 raymarch(vec3 startLoc, vec3 step, vec3 viewDir) {
                 #if defined(dColorType_objectPicking) || defined(dColorType_instancePicking) || defined(dColorType_groupPicking)
                     if (uAlpha < uPickingAlphaThreshold)
                         discard; // ignore so the element below can be picked
-                #else
+                    if (uPickable == 0)
+                        return vec4(0.0, 0.0, 0.0, 1.0); // set to empty picking id
+                #endif
 
                 #if defined(dColorType_objectPicking)
                     return vec4(encodeIdRGB(float(uObjectId)), 1.0);

+ 52 - 32
src/mol-gl/webgl/context.ts

@@ -55,39 +55,42 @@ function unbindFramebuffer(gl: GLRenderingContext) {
 
 const tmpPixel = new Uint8Array(1 * 4);
 
-function fence(gl: WebGL2RenderingContext) {
+function checkSync(gl: WebGL2RenderingContext, sync: WebGLSync, resolve: () => void) {
+    if (gl.getSyncParameter(sync, gl.SYNC_STATUS) === gl.SIGNALED) {
+        gl.deleteSync(sync)
+        resolve()
+    } else {
+        Scheduler.setImmediate(checkSync, gl, sync, resolve)
+    }
+}
+
+function fence(gl: WebGL2RenderingContext, resolve: () => void) {
+    const sync = gl.fenceSync(gl.SYNC_GPU_COMMANDS_COMPLETE, 0)
+    if (!sync) {
+        console.warn('Could not create a WebGLSync object')
+        gl.readPixels(0, 0, 1, 1, gl.RGBA, gl.UNSIGNED_BYTE, tmpPixel)
+        resolve()
+    } else {
+        Scheduler.setImmediate(checkSync, gl, sync, resolve)
+    }
+}
+
+let SentWebglSyncObjectNotSupportedInWebglMessage = false
+function waitForGpuCommandsComplete(gl: GLRenderingContext): Promise<void> {
     return new Promise(resolve => {
-        gl.finish()
-        const sync = gl.fenceSync(gl.SYNC_GPU_COMMANDS_COMPLETE, 0)
-        if (!sync) {
-            console.warn('could not create a WebGL2 sync object')
-            gl.readPixels(0, 0, 1, 1, gl.RGBA, gl.UNSIGNED_BYTE, tmpPixel)
-            resolve()
+        if (isWebGL2(gl)) {
+            fence(gl, resolve)
         } else {
-            gl.flush(); // Ensure the fence is submitted.
-            const check = () => {
-                const status = gl.getSyncParameter(sync, gl.SYNC_STATUS)
-                if (status === gl.SIGNALED) {
-                    gl.deleteSync(sync)
-                    resolve()
-                } else {
-                    Scheduler.setImmediate(check, 0)
-                }
+            if (!SentWebglSyncObjectNotSupportedInWebglMessage) {
+                console.info('Sync object not supported in WebGL')
+                SentWebglSyncObjectNotSupportedInWebglMessage = true
             }
-            Scheduler.setImmediate(check, 0)
+            gl.readPixels(0, 0, 1, 1, gl.RGBA, gl.UNSIGNED_BYTE, tmpPixel)
+            resolve()
         }
     })
 }
 
-async function waitForGpuCommandsComplete(gl: GLRenderingContext) {
-    if (isWebGL2(gl)) {
-        await fence(gl)
-    } else {
-        console.info('webgl sync object not supported in webgl 1')
-        gl.readPixels(0, 0, 1, 1, gl.RGBA, gl.UNSIGNED_BYTE, tmpPixel)
-    }
-}
-
 export function createImageData(buffer: ArrayLike<number>, width: number, height: number) {
     const w = width * 4
     const h = height
@@ -198,17 +201,34 @@ export function createContext(gl: GLRenderingContext): WebGLContext {
     let readPixelsAsync: (x: number, y: number, width: number, height: number, buffer: Uint8Array) => Promise<void>
     if (isWebGL2(gl)) {
         const pbo = gl.createBuffer()
-        readPixelsAsync = async (x: number, y: number, width: number, height: number, buffer: Uint8Array) => {
+        let _buffer: Uint8Array | undefined = void 0
+        let _resolve: (() => void) | undefined = void 0
+        let _reading = false
+
+        const bindPBO = () => {
+            gl.bindBuffer(gl.PIXEL_PACK_BUFFER, pbo)
+            gl.getBufferSubData(gl.PIXEL_PACK_BUFFER, 0, _buffer!)
+            gl.bindBuffer(gl.PIXEL_PACK_BUFFER, null)
+            _reading = false
+            _resolve!()
+            _resolve = void 0
+            _buffer = void 0
+        }
+        readPixelsAsync = (x: number, y: number, width: number, height: number, buffer: Uint8Array): Promise<void> => new Promise<void>((resolve, reject) => {
+            if (_reading) {
+                reject('Can not call multiple readPixelsAsync at the same time')
+                return
+            }
+            _reading = true;
             gl.bindBuffer(gl.PIXEL_PACK_BUFFER, pbo)
             gl.bufferData(gl.PIXEL_PACK_BUFFER, width * height * 4, gl.STREAM_READ)
             gl.readPixels(x, y, width, height, gl.RGBA, gl.UNSIGNED_BYTE, 0)
             gl.bindBuffer(gl.PIXEL_PACK_BUFFER, null)
             // need to unbind/bind PBO before/after async awaiting the fence
-            await fence(gl)
-            gl.bindBuffer(gl.PIXEL_PACK_BUFFER, pbo)
-            gl.getBufferSubData(gl.PIXEL_PACK_BUFFER, 0, buffer)
-            gl.bindBuffer(gl.PIXEL_PACK_BUFFER, null)
-        }
+            _resolve = resolve
+            _buffer = buffer
+            fence(gl, bindPBO)
+        })
     } else {
         readPixelsAsync = async (x: number, y: number, width: number, height: number, buffer: Uint8Array) => {
             gl.readPixels(x, y, width, height, gl.RGBA, gl.UNSIGNED_BYTE, buffer)

+ 3 - 0
src/mol-gl/webgl/program.ts

@@ -68,6 +68,9 @@ export function createProgram(ctx: WebGLContext, props: ProgramProps): Program {
     vertShaderRef.value.attach(program)
     fragShaderRef.value.attach(program)
     gl.linkProgram(program)
+    if (!gl.getProgramParameter(program, gl.LINK_STATUS)){
+        throw new Error(`Could not compile WebGL program. \n\n${gl.getProgramInfoLog(program)}`);
+    }
 
     const uniformUpdaters = getUniformUpdaters(ctx, program, schema)
     const attributeLocations = getAttributeLocations(ctx, program, schema)

+ 3 - 3
src/mol-math/geometry/gaussian-density/cpu.ts

@@ -38,7 +38,7 @@ export async function GaussianDensityCPU(ctx: RuntimeContext, position: Position
     const delta = getDelta(Box3D.expand(Box3D.empty(), box, Vec3.create(pad, pad, pad)), resolution)
     const dim = Vec3.zero()
     Vec3.ceil(dim, Vec3.mul(dim, extent, delta))
-    console.log('grid dim', dim)
+    // console.log('grid dim', dim)
 
     const space = Tensor.Space(dim, [0, 1, 2], Float32Array)
     const data = space.create()
@@ -63,7 +63,7 @@ export async function GaussianDensityCPU(ctx: RuntimeContext, position: Position
 
     const gridPad = 1 / Math.max(...delta)
 
-    console.time('gaussian density cpu')
+    // console.time('gaussian density cpu')
     for (let i = 0; i < n; ++i) {
         const j = OrderedSet.getAt(indices, i)
 
@@ -105,7 +105,7 @@ export async function GaussianDensityCPU(ctx: RuntimeContext, position: Position
             await ctx.update({ message: 'filling density grid', current: i, max: n })
         }
     }
-    console.timeEnd('gaussian density cpu')
+    // console.timeEnd('gaussian density cpu')
 
     const transform = Mat4.identity()
     Mat4.fromScaling(transform, Vec3.inverse(Vec3.zero(), delta))

+ 0 - 1
src/mol-math/geometry/gaussian-density/gpu.ts

@@ -233,7 +233,6 @@ function getGaussianDensityRenderObject(webgl: WebGLContext, drawCount: number,
     const state: RenderableState = {
         visible: true,
         pickable: false,
-        depthMask: false
     }
 
     const renderObject = createGaussianDensityRenderObject(values, state)

+ 30 - 7
src/mol-model/structure/structure/unit/links/data.ts

@@ -23,6 +23,7 @@ class InterUnitBonds {
     /** Array of inter-unit bonds */
     readonly bonds: ReadonlyArray<InterUnitBonds.Bond>
     private readonly bondKeyIndex: Map<string, number>
+    private readonly elementKeyIndex: Map<string, number[]>
 
     /** Get an array of unit-pair-bonds that are linked to the given unit */
     getLinkedUnits(unit: Unit): ReadonlyArray<InterUnitBonds.UnitPairBonds> {
@@ -32,8 +33,8 @@ class InterUnitBonds {
 
     /** Index into this.bonds */
     getBondIndex(indexA: StructureElement.UnitIndex, unitA: Unit, indexB: StructureElement.UnitIndex, unitB: Unit): number {
-        const key = InterUnitBonds.getBondKey(indexA, unitA, indexB, unitB)
-        const index = this.bondKeyIndex.get(key)
+        const bondKey = InterUnitBonds.getBondKey(indexA, unitA, indexB, unitB)
+        const index = this.bondKeyIndex.get(bondKey)
         return index !== undefined ? index : -1
     }
 
@@ -48,26 +49,44 @@ class InterUnitBonds {
         return this.getBond(l.aIndex, l.aUnit, l.bIndex, l.bUnit);
     }
 
+    /** Indices into this.bonds */
+    getBondIndices(index: StructureElement.UnitIndex, unit: Unit): ReadonlyArray<number> {
+        const elementKey = InterUnitBonds.getElementKey(index, unit)
+        const indices = this.elementKeyIndex.get(elementKey)
+        return indices !== undefined ? indices : []
+    }
+
     constructor(private map: Map<number, InterUnitBonds.UnitPairBonds[]>) {
         let count = 0
         const bonds: (InterUnitBonds.Bond)[] = []
         const bondKeyIndex = new Map<string, number>()
+        const elementKeyIndex = new Map<string, number[]>()
+
         this.map.forEach(pairBondsArray => {
             pairBondsArray.forEach(pairBonds => {
                 count += pairBonds.bondCount
                 pairBonds.linkedElementIndices.forEach(indexA => {
                     pairBonds.getBonds(indexA).forEach(bondInfo => {
                         const { unitA, unitB } = pairBonds
-                        const key = InterUnitBonds.getBondKey(indexA, unitA, bondInfo.indexB, unitB)
-                        bondKeyIndex.set(key, bonds.length)
+                        
+                        const bondKey = InterUnitBonds.getBondKey(indexA, unitA, bondInfo.indexB, unitB)
+                        bondKeyIndex.set(bondKey, bonds.length)
+                        
+                        const elementKey = InterUnitBonds.getElementKey(indexA, unitA)
+                        const e = elementKeyIndex.get(elementKey)
+                        if (e === undefined) elementKeyIndex.set(elementKey, [bonds.length])
+                        else e.push(bonds.length)
+
                         bonds.push({ ...bondInfo, indexA, unitA, unitB })
                     })
                 })
             })
         })
+
         this.bondCount = count
         this.bonds = bonds
         this.bondKeyIndex = bondKeyIndex
+        this.elementKeyIndex = elementKeyIndex
     }
 }
 
@@ -102,15 +121,19 @@ namespace InterUnitBonds {
     export interface Bond {
         readonly unitA: Unit.Atomic,
         readonly unitB: Unit.Atomic,
-        readonly indexA: number,
-        readonly indexB: number,
+        readonly indexA: StructureElement.UnitIndex,
+        readonly indexB: StructureElement.UnitIndex,
         readonly order: number,
         readonly flag: LinkType.Flag
     }
 
-    export function getBondKey(indexA: number, unitA: Unit, indexB: number, unitB: Unit) {
+    export function getBondKey(indexA: StructureElement.UnitIndex, unitA: Unit, indexB: StructureElement.UnitIndex, unitB: Unit) {
         return `${indexA}|${unitA.id}|${indexB}|${unitB.id}`
     }
+
+    export function getElementKey(index: StructureElement.UnitIndex, unit: Unit) {
+        return `${index}|${unit.id}`
+    }
 }
 
 const emptyArray: any[] = [];

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

@@ -30,7 +30,8 @@ export const BallAndStickParams = {
     ...IntraUnitLinkParams,
     ...InterUnitLinkParams,
     unitKinds: PD.MultiSelect<UnitKind>(['atomic'], UnitKindOptions),
-    sizeFactor: PD.Numeric(0.2, { min: 0.01, max: 10, step: 0.01 }),
+    sizeFactor: PD.Numeric(0.3, { min: 0.01, max: 10, step: 0.01 }),
+    sizeAspectRatio: PD.Numeric(2/3, { min: 0.01, max: 3, step: 0.01 }),
     colorTheme: PD.Mapped('polymer-index', BuiltInColorThemeOptions, name => PD.Group((BuiltInColorThemes as { [k: string]: ColorTheme.Provider<any> })[name].getParams({}))),
     visuals: PD.MultiSelect<BallAndStickVisualName>(['element-sphere', 'intra-link', 'inter-link'], BallAndStickVisualOptions),
 }

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

@@ -51,7 +51,6 @@ async function createCarbohydrateLinkCylinderMesh(ctx: VisualContext, structure:
 export const CarbohydrateLinkParams = {
     ...UnitsMeshParams,
     ...LinkCylinderParams,
-    detail: PD.Numeric(0, { min: 0, max: 3, step: 1 }),
     linkSizeFactor: PD.Numeric(0.3, { min: 0, max: 3, step: 0.01 }),
 }
 export type CarbohydrateLinkParams = typeof CarbohydrateLinkParams
@@ -64,7 +63,10 @@ export function CarbohydrateLinkVisual(): ComplexVisual<CarbohydrateLinkParams>
         getLoci: getLinkLoci,
         mark: markLink,
         setUpdateState: (state: VisualUpdateState, newProps: PD.Values<CarbohydrateLinkParams>, currentProps: PD.Values<CarbohydrateLinkParams>) => {
-            state.createGeometry = newProps.radialSegments !== currentProps.radialSegments
+            state.createGeometry = (
+                newProps.linkSizeFactor !== currentProps.linkSizeFactor ||
+                newProps.radialSegments !== currentProps.radialSegments
+            )
         }
     })
 }

+ 4 - 1
src/mol-repr/structure/visual/carbohydrate-symbol-mesh.ts

@@ -159,7 +159,10 @@ export function CarbohydrateSymbolVisual(): ComplexVisual<CarbohydrateSymbolPara
         getLoci: getCarbohydrateLoci,
         mark: markCarbohydrate,
         setUpdateState: (state: VisualUpdateState, newProps: PD.Values<CarbohydrateSymbolParams>, currentProps: PD.Values<CarbohydrateSymbolParams>) => {
-            state.createGeometry = newProps.detail !== currentProps.detail
+            state.createGeometry = (
+                newProps.sizeFactor !== currentProps.sizeFactor ||
+                newProps.detail !== currentProps.detail
+            )
         }
     })
 }

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

@@ -61,7 +61,6 @@ async function createCarbohydrateTerminalLinkCylinderMesh(ctx: VisualContext, st
 export const CarbohydrateTerminalLinkParams = {
     ...UnitsMeshParams,
     ...LinkCylinderParams,
-    detail: PD.Numeric(0, { min: 0, max: 3, step: 1 }),
     linkSizeFactor: PD.Numeric(0.3, { min: 0, max: 3, step: 0.01 }),
 }
 export type CarbohydrateTerminalLinkParams = typeof CarbohydrateTerminalLinkParams
@@ -74,7 +73,10 @@ export function CarbohydrateTerminalLinkVisual(): ComplexVisual<CarbohydrateTerm
         getLoci: getTerminalLinkLoci,
         mark: markTerminalLink,
         setUpdateState: (state: VisualUpdateState, newProps: PD.Values<CarbohydrateTerminalLinkParams>, currentProps: PD.Values<CarbohydrateTerminalLinkParams>) => {
-            state.createGeometry = newProps.radialSegments !== currentProps.radialSegments
+            state.createGeometry = (
+                newProps.linkSizeFactor !== currentProps.linkSizeFactor ||
+                newProps.radialSegments !== currentProps.radialSegments
+            )
         }
     })
 }

+ 9 - 4
src/mol-repr/structure/visual/cross-link-restraint-cylinder.ts

@@ -7,7 +7,7 @@
 import { Link, Structure, StructureElement } from 'mol-model/structure';
 import { ComplexVisual } from '../representation';
 import { VisualUpdateState } from '../../util';
-import { LinkCylinderProps, createLinkCylinderMesh, LinkCylinderParams } from './util/link';
+import { createLinkCylinderMesh, LinkCylinderParams } from './util/link';
 import { Vec3 } from 'mol-math/linear-algebra';
 import { Loci, EmptyLoci } from 'mol-model/loci';
 import { ComplexMeshVisual, ComplexMeshParams } from '../complex-visual';
@@ -21,10 +21,11 @@ import { PickingId } from 'mol-geo/geometry/picking';
 import { VisualContext } from 'mol-repr/representation';
 import { Theme } from 'mol-theme/theme';
 
-async function createCrossLinkRestraintCylinderMesh(ctx: VisualContext, structure: Structure, theme: Theme, props: LinkCylinderProps, mesh?: Mesh) {
+async function createCrossLinkRestraintCylinderMesh(ctx: VisualContext, structure: Structure, theme: Theme, props: PD.Values<CrossLinkRestraintParams>, mesh?: Mesh) {
 
     const crossLinks = structure.crossLinkRestraints
     if (!crossLinks.count) return Mesh.createEmpty(mesh)
+    const { sizeFactor } = props
 
     const location = StructureElement.create()
 
@@ -43,7 +44,7 @@ async function createCrossLinkRestraintCylinderMesh(ctx: VisualContext, structur
             const b = crossLinks.pairs[edgeIndex]
             location.unit = b.unitA
             location.element = b.unitA.elements[b.indexA]
-            return theme.size.size(location)
+            return theme.size.size(location) * sizeFactor
         }
     }
 
@@ -53,6 +54,7 @@ async function createCrossLinkRestraintCylinderMesh(ctx: VisualContext, structur
 export const CrossLinkRestraintParams = {
     ...ComplexMeshParams,
     ...LinkCylinderParams,
+    sizeFactor: PD.Numeric(1, { min: 0, max: 10, step: 0.1 }),
 }
 export type CrossLinkRestraintParams = typeof CrossLinkRestraintParams
 
@@ -64,7 +66,10 @@ export function CrossLinkRestraintVisual(): ComplexVisual<CrossLinkRestraintPara
         getLoci: getLinkLoci,
         mark: markLink,
         setUpdateState: (state: VisualUpdateState, newProps: PD.Values<CrossLinkRestraintParams>, currentProps: PD.Values<CrossLinkRestraintParams>) => {
-            state.createGeometry = newProps.radialSegments !== currentProps.radialSegments
+            state.createGeometry = (
+                newProps.sizeFactor !== currentProps.sizeFactor ||
+                newProps.radialSegments !== currentProps.radialSegments
+            )
         }
     })
 }

+ 4 - 1
src/mol-repr/structure/visual/element-sphere.ts

@@ -28,7 +28,10 @@ export function ElementSphereVisual(): UnitsVisual<ElementSphereParams> {
         getLoci: getElementLoci,
         mark: markElement,
         setUpdateState: (state: VisualUpdateState, newProps: PD.Values<ElementSphereParams>, currentProps: PD.Values<ElementSphereParams>) => {
-            state.createGeometry = newProps.detail !== currentProps.detail
+            state.createGeometry = (
+                newProps.sizeFactor !== currentProps.sizeFactor ||
+                newProps.detail !== currentProps.detail
+            )
         }
     })
 }

+ 33 - 12
src/mol-repr/structure/visual/inter-unit-link-cylinder.ts

@@ -11,7 +11,7 @@ import { createLinkCylinderMesh, LinkIterator, LinkCylinderParams } from './util
 import { Vec3 } from 'mol-math/linear-algebra';
 import { Loci, EmptyLoci } from 'mol-model/loci';
 import { ComplexMeshVisual, ComplexMeshParams } from '../complex-visual';
-import { Interval } from 'mol-data/int';
+import { Interval, OrderedSet } from 'mol-data/int';
 import { BitFlags } from 'mol-util';
 import { ParamDefinition as PD } from 'mol-util/param-definition';
 import { Mesh } from 'mol-geo/geometry/mesh/mesh';
@@ -22,7 +22,7 @@ import { Theme } from 'mol-theme/theme';
 async function createInterUnitLinkCylinderMesh(ctx: VisualContext, structure: Structure, theme: Theme, props: PD.Values<InterUnitLinkParams>, mesh?: Mesh) {
     const links = structure.links
     const { bondCount, bonds } = links
-    const { sizeFactor } = props
+    const { sizeFactor, sizeAspectRatio } = props
 
     if (!bondCount) return Mesh.createEmpty(mesh)
 
@@ -43,7 +43,7 @@ async function createInterUnitLinkCylinderMesh(ctx: VisualContext, structure: St
             const b = bonds[edgeIndex]
             location.unit = b.unitA
             location.element = b.unitA.elements[b.indexA]
-            return theme.size.size(location) * sizeFactor
+            return theme.size.size(location) * sizeFactor * sizeAspectRatio
         }
     }
 
@@ -53,7 +53,8 @@ async function createInterUnitLinkCylinderMesh(ctx: VisualContext, structure: St
 export const InterUnitLinkParams = {
     ...ComplexMeshParams,
     ...LinkCylinderParams,
-    sizeFactor: PD.Numeric(0.2, { min: 0, max: 10, step: 0.01 }),
+    sizeFactor: PD.Numeric(0.3, { min: 0, max: 10, step: 0.01 }),
+    sizeAspectRatio: PD.Numeric(2/3, { min: 0, max: 3, step: 0.01 }),
 }
 export type InterUnitLinkParams = typeof InterUnitLinkParams
 
@@ -65,9 +66,13 @@ export function InterUnitLinkVisual(): ComplexVisual<InterUnitLinkParams> {
         getLoci: getLinkLoci,
         mark: markLink,
         setUpdateState: (state: VisualUpdateState, newProps: PD.Values<InterUnitLinkParams>, currentProps: PD.Values<InterUnitLinkParams>) => {
-            if (newProps.linkScale !== currentProps.linkScale) state.createGeometry = true
-            if (newProps.linkSpacing !== currentProps.linkSpacing) state.createGeometry = true
-            if (newProps.radialSegments !== currentProps.radialSegments) state.createGeometry = true
+            state.createGeometry = (
+                newProps.sizeFactor !== currentProps.sizeFactor ||
+                newProps.sizeAspectRatio !== currentProps.sizeAspectRatio ||
+                newProps.radialSegments !== currentProps.radialSegments ||
+                newProps.linkScale !== currentProps.linkScale ||
+                newProps.linkSpacing !== currentProps.linkSpacing
+            )
         }
     })
 }
@@ -80,6 +85,10 @@ function getLinkLoci(pickingId: PickingId, structure: Structure, id: number) {
             Link.Location(
                 bond.unitA, bond.indexA as StructureElement.UnitIndex,
                 bond.unitB, bond.indexB as StructureElement.UnitIndex
+            ),
+            Link.Location(
+                bond.unitB, bond.indexB as StructureElement.UnitIndex,
+                bond.unitA, bond.indexA as StructureElement.UnitIndex
             )
         ])
     }
@@ -88,11 +97,23 @@ function getLinkLoci(pickingId: PickingId, structure: Structure, id: number) {
 
 function markLink(loci: Loci, structure: Structure, apply: (interval: Interval) => boolean) {
     let changed = false
-    if (!Link.isLoci(loci)) return false
-    for (const b of loci.links) {
-        const idx = structure.links.getBondIndex(b.aIndex, b.aUnit, b.bIndex, b.bUnit)
-        if (idx !== -1) {
-            if (apply(Interval.ofSingleton(idx))) changed = true
+    if (Link.isLoci(loci)) {
+        if (loci.structure !== structure) return false
+        for (const b of loci.links) {
+            const idx = structure.links.getBondIndex(b.aIndex, b.aUnit, b.bIndex, b.bUnit)
+            if (idx !== -1) {
+                if (apply(Interval.ofSingleton(idx))) changed = true
+            }
+        }
+    } else if (StructureElement.isLoci(loci)) {
+        if (loci.structure !== structure) return false
+        for (const e of loci.elements) {
+            OrderedSet.forEach(e.indices, v => {
+                const indices = structure.links.getBondIndices(v, e.unit)
+                for (let i = 0, il = indices.length; i < il; ++i) {
+                    if (apply(Interval.ofSingleton(indices[i]))) changed = true
+                }
+            })
         }
     }
     return changed

+ 48 - 21
src/mol-repr/structure/visual/intra-unit-link-cylinder.ts

@@ -8,11 +8,11 @@
 import { Unit, Link, StructureElement, Structure } from 'mol-model/structure';
 import { UnitsVisual } from '../representation';
 import { VisualUpdateState } from '../../util';
-import { LinkCylinderProps, createLinkCylinderMesh, LinkIterator, LinkCylinderParams } from './util/link';
+import { createLinkCylinderMesh, LinkIterator, LinkCylinderParams } from './util/link';
 import { Vec3 } from 'mol-math/linear-algebra';
 import { Loci, EmptyLoci } from 'mol-model/loci';
 import { UnitsMeshVisual, UnitsMeshParams, StructureGroup } from '../units-visual';
-import { Interval } from 'mol-data/int';
+import { Interval, OrderedSet } from 'mol-data/int';
 import { BitFlags } from 'mol-util';
 import { ParamDefinition as PD } from 'mol-util/param-definition';
 import { Mesh } from 'mol-geo/geometry/mesh/mesh';
@@ -29,7 +29,7 @@ async function createIntraUnitLinkCylinderMesh(ctx: VisualContext, unit: Unit, s
     const links = unit.links
     const { edgeCount, a, b, edgeProps, offset } = links
     const { order: _order, flags: _flags } = edgeProps
-    const { sizeFactor } = props
+    const { sizeFactor, sizeAspectRatio } = props
 
     if (!edgeCount) return Mesh.createEmpty(mesh)
 
@@ -57,7 +57,7 @@ async function createIntraUnitLinkCylinderMesh(ctx: VisualContext, unit: Unit, s
         flags: (edgeIndex: number) => BitFlags.create(_flags[edgeIndex]),
         radius: (edgeIndex: number) => {
             location.element = elements[a[edgeIndex]]
-            return theme.size.size(location) * sizeFactor
+            return theme.size.size(location) * sizeFactor * sizeAspectRatio
         }
     }
 
@@ -67,7 +67,8 @@ async function createIntraUnitLinkCylinderMesh(ctx: VisualContext, unit: Unit, s
 export const IntraUnitLinkParams = {
     ...UnitsMeshParams,
     ...LinkCylinderParams,
-    sizeFactor: PD.Numeric(0.2, { min: 0, max: 10, step: 0.01 }),
+    sizeFactor: PD.Numeric(0.3, { min: 0, max: 10, step: 0.01 }),
+    sizeAspectRatio: PD.Numeric(2/3, { min: 0, max: 3, step: 0.01 }),
 }
 export type IntraUnitLinkParams = typeof IntraUnitLinkParams
 
@@ -78,10 +79,14 @@ export function IntraUnitLinkVisual(): UnitsVisual<IntraUnitLinkParams> {
         createLocationIterator: LinkIterator.fromGroup,
         getLoci: getLinkLoci,
         mark: markLink,
-        setUpdateState: (state: VisualUpdateState, newProps: LinkCylinderProps, currentProps: LinkCylinderProps) => {
-            if (newProps.linkScale !== currentProps.linkScale) state.createGeometry = true
-            if (newProps.linkSpacing !== currentProps.linkSpacing) state.createGeometry = true
-            if (newProps.radialSegments !== currentProps.radialSegments) state.createGeometry = true
+        setUpdateState: (state: VisualUpdateState, newProps: PD.Values<IntraUnitLinkParams>, currentProps: PD.Values<IntraUnitLinkParams>) => {
+            state.createGeometry = (
+                newProps.sizeFactor !== currentProps.sizeFactor ||
+                newProps.sizeAspectRatio !== currentProps.sizeAspectRatio ||
+                newProps.radialSegments !== currentProps.radialSegments ||
+                newProps.linkScale !== currentProps.linkScale ||
+                newProps.linkSpacing !== currentProps.linkSpacing
+            )
         }
     })
 }
@@ -96,6 +101,10 @@ function getLinkLoci(pickingId: PickingId, structureGroup: StructureGroup, id: n
                 Link.Location(
                     unit, unit.links.a[groupId] as StructureElement.UnitIndex,
                     unit, unit.links.b[groupId] as StructureElement.UnitIndex
+                ),
+                Link.Location(
+                    unit, unit.links.b[groupId] as StructureElement.UnitIndex,
+                    unit, unit.links.a[groupId] as StructureElement.UnitIndex
                 )
             ])
         }
@@ -105,18 +114,36 @@ function getLinkLoci(pickingId: PickingId, structureGroup: StructureGroup, id: n
 
 function markLink(loci: Loci, structureGroup: StructureGroup, apply: (interval: Interval) => boolean) {
     let changed = false
-    if (!Link.isLoci(loci)) return false
-    const { structure, group } = structureGroup
-    if (loci.structure !== structure) return false
-    const unit = group.units[0]
-    if (!Unit.isAtomic(unit)) return false
-    const groupCount = unit.links.edgeCount * 2
-    for (const b of loci.links) {
-        const unitIdx = group.unitIndexMap.get(b.aUnit.id)
-        if (unitIdx !== undefined) {
-            const idx = unit.links.getDirectedEdgeIndex(b.aIndex, b.bIndex)
-            if (idx !== -1) {
-                if (apply(Interval.ofSingleton(unitIdx * groupCount + idx))) changed = true
+    if (Link.isLoci(loci)) {
+        const { structure, group } = structureGroup
+        if (loci.structure !== structure) return false
+        const unit = group.units[0]
+        if (!Unit.isAtomic(unit)) return false
+        const groupCount = unit.links.edgeCount * 2
+        for (const b of loci.links) {
+            const unitIdx = group.unitIndexMap.get(b.aUnit.id)
+            if (unitIdx !== undefined) {
+                const idx = unit.links.getDirectedEdgeIndex(b.aIndex, b.bIndex)
+                if (idx !== -1) {
+                    if (apply(Interval.ofSingleton(unitIdx * groupCount + idx))) changed = true
+                }
+            }
+        }
+    } else if (StructureElement.isLoci(loci)) {
+        const { structure, group } = structureGroup
+        if (loci.structure !== structure) return false
+        const unit = group.units[0]
+        if (!Unit.isAtomic(unit)) return false
+        const groupCount = unit.links.edgeCount * 2
+        for (const e of loci.elements) {
+            const unitIdx = group.unitIndexMap.get(e.unit.id)
+            if (unitIdx !== undefined) {
+                const { offset } = unit.links
+                OrderedSet.forEach(e.indices, v => {
+                    for (let t = offset[v], _t = offset[v + 1]; t < _t; t++) {
+                        if (apply(Interval.ofSingleton(unitIdx * groupCount + t))) changed = true
+                    }
+                })
             }
         }
     }

+ 6 - 1
src/mol-repr/structure/visual/nucleotide-block-mesh.ts

@@ -19,6 +19,7 @@ import { MeshBuilder } from 'mol-geo/geometry/mesh/mesh-builder';
 import { addCylinder } from 'mol-geo/geometry/mesh/builder/cylinder';
 import { VisualContext } from 'mol-repr/representation';
 import { Theme } from 'mol-theme/theme';
+import { VisualUpdateState } from 'mol-repr/util';
 
 const p1 = Vec3.zero()
 const p2 = Vec3.zero()
@@ -132,6 +133,10 @@ export function NucleotideBlockVisual(): UnitsVisual<NucleotideBlockParams> {
         createLocationIterator: NucleotideLocationIterator.fromGroup,
         getLoci: getNucleotideElementLoci,
         mark: markNucleotideElement,
-        setUpdateState: () => {}
+        setUpdateState: (state: VisualUpdateState, newProps: PD.Values<NucleotideBlockParams>, currentProps: PD.Values<NucleotideBlockParams>) => {
+            state.createGeometry = (
+                newProps.sizeFactor !== currentProps.sizeFactor
+            )
+        }
     })
 }

+ 8 - 4
src/mol-repr/structure/visual/polymer-backbone-cylinder.ts

@@ -21,6 +21,7 @@ import { VisualContext } from 'mol-repr/representation';
 import { Theme } from 'mol-theme/theme';
 
 export const PolymerBackboneCylinderParams = {
+    sizeFactor: PD.Numeric(0.3, { min: 0, max: 10, step: 0.01 }),
     radialSegments: PD.Numeric(16, { min: 3, max: 56, step: 1 }),
 }
 export const DefaultPolymerBackboneCylinderProps = PD.getDefaultValues(PolymerBackboneCylinderParams)
@@ -30,7 +31,7 @@ async function createPolymerBackboneCylinderMesh(ctx: VisualContext, unit: Unit,
     const polymerElementCount = unit.polymerElements.length
     if (!polymerElementCount) return Mesh.createEmpty(mesh)
 
-    const { radialSegments } = props
+    const { radialSegments, sizeFactor } = props
 
     const vertexCountEstimate = radialSegments * 2 * polymerElementCount * 2
     const builder = MeshBuilder.create(vertexCountEstimate, vertexCountEstimate / 10, mesh)
@@ -48,11 +49,11 @@ async function createPolymerBackboneCylinderMesh(ctx: VisualContext, unit: Unit,
         pos(centerA.element, pA)
         pos(centerB.element, pB)
 
-        cylinderProps.radiusTop = cylinderProps.radiusBottom = theme.size.size(centerA)
+        cylinderProps.radiusTop = cylinderProps.radiusBottom = theme.size.size(centerA) * sizeFactor
         builder.setGroup(OrderedSet.indexOf(elements, centerA.element))
         addCylinder(builder, pA, pB, 0.5, cylinderProps)
 
-        cylinderProps.radiusTop = cylinderProps.radiusBottom = theme.size.size(centerB)
+        cylinderProps.radiusTop = cylinderProps.radiusBottom = theme.size.size(centerB) * sizeFactor
         builder.setGroup(OrderedSet.indexOf(elements, centerB.element))
         addCylinder(builder, pB, pA, 0.5, cylinderProps)
 
@@ -80,7 +81,10 @@ export function PolymerBackboneVisual(): UnitsVisual<PolymerBackboneParams> {
         getLoci: getElementLoci,
         mark: markElement,
         setUpdateState: (state: VisualUpdateState, newProps: PD.Values<PolymerBackboneParams>, currentProps: PD.Values<PolymerBackboneParams>) => {
-            state.createGeometry = newProps.radialSegments !== currentProps.radialSegments
+            state.createGeometry = (
+                newProps.sizeFactor !== currentProps.sizeFactor ||
+                newProps.radialSegments !== currentProps.radialSegments
+            )
         }
     })
 }

+ 6 - 1
src/mol-repr/structure/visual/polymer-direction-wedge.ts

@@ -16,6 +16,7 @@ import { Mesh } from 'mol-geo/geometry/mesh/mesh';
 import { MeshBuilder } from 'mol-geo/geometry/mesh/mesh-builder';
 import { VisualContext } from 'mol-repr/representation';
 import { Theme } from 'mol-theme/theme';
+import { VisualUpdateState } from 'mol-repr/util';
 
 const t = Mat4.identity()
 const sVec = Vec3.zero()
@@ -101,6 +102,10 @@ export function PolymerDirectionVisual(): UnitsVisual<PolymerDirectionParams> {
         createLocationIterator: PolymerLocationIterator.fromGroup,
         getLoci: getPolymerElementLoci,
         mark: markPolymerElement,
-        setUpdateState: () => {}
+        setUpdateState: (state: VisualUpdateState, newProps: PD.Values<PolymerDirectionParams>, currentProps: PD.Values<PolymerDirectionParams>) => {
+            state.createGeometry = (
+                newProps.sizeFactor !== currentProps.sizeFactor
+            )
+        }
     })
 }

+ 4 - 1
src/mol-repr/structure/visual/polymer-gap-cylinder.ts

@@ -96,7 +96,10 @@ export function PolymerGapVisual(): UnitsVisual<PolymerGapParams> {
         getLoci: getPolymerGapElementLoci,
         mark: markPolymerGapElement,
         setUpdateState: (state: VisualUpdateState, newProps: PD.Values<PolymerGapParams>, currentProps: PD.Values<PolymerGapParams>) => {
-            state.createGeometry = newProps.radialSegments !== currentProps.radialSegments
+            state.createGeometry = (
+                newProps.sizeFactor !== currentProps.sizeFactor ||
+                newProps.radialSegments !== currentProps.radialSegments
+            )
         }
     })
 }

+ 1 - 0
src/mol-repr/structure/visual/polymer-trace-mesh.ts

@@ -101,6 +101,7 @@ export function PolymerTraceVisual(): UnitsVisual<PolymerTraceParams> {
         mark: markPolymerElement,
         setUpdateState: (state: VisualUpdateState, newProps: PD.Values<PolymerTraceParams>, currentProps: PD.Values<PolymerTraceParams>) => {
             state.createGeometry = (
+                newProps.sizeFactor !== currentProps.sizeFactor ||
                 newProps.linearSegments !== currentProps.linearSegments ||
                 newProps.radialSegments !== currentProps.radialSegments ||
                 newProps.aspectRatio !== currentProps.aspectRatio ||

+ 28 - 21
src/mol-repr/structure/visual/util/polymer.ts

@@ -6,11 +6,12 @@
 
 import { Unit, ElementIndex, StructureElement, Link } from 'mol-model/structure';
 import SortedRanges from 'mol-data/int/sorted-ranges';
-import { OrderedSet, Interval } from 'mol-data/int';
+import { OrderedSet, Interval, SortedArray } from 'mol-data/int';
 import { EmptyLoci, Loci } from 'mol-model/loci';
 import { LocationIterator } from 'mol-geo/util/location-iterator';
 import { PickingId } from 'mol-geo/geometry/picking';
 import { StructureGroup } from 'mol-repr/structure/units-visual';
+import { getElementIndexForAtomRole } from 'mol-model/structure/util';
 
 export * from './polymer/backbone-iterator'
 export * from './polymer/gap-iterator'
@@ -65,17 +66,22 @@ export namespace PolymerGapLocationIterator {
     }
 }
 
+/** Return a Loci for the elements of a whole residue. */
 export function getPolymerElementLoci(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 === undefined) {
-            console.log(id, { objectId, instanceId, groupId }, group.units)
-        }
-        const unitIndex = OrderedSet.indexOf(unit.elements, unit.polymerElements[groupId]) as StructureElement.UnitIndex
-        if (unitIndex !== -1) {
-            const indices = OrderedSet.ofSingleton(unitIndex)
+        const { elements, polymerElements, model } = unit
+        if (OrderedSet.indexOf(elements, polymerElements[groupId]) !== -1) {
+            const { index, offsets } = model.atomicHierarchy.residueAtomSegments
+            const rI = index[polymerElements[groupId]]
+            const _indices: number[] = []
+            for (let i = offsets[rI], il = offsets[rI + 1]; i < il; ++i) {
+                const unitIndex = OrderedSet.indexOf(elements, i)
+                if (unitIndex !== -1) _indices.push(unitIndex)
+            }
+            const indices = OrderedSet.ofSortedArray<StructureElement.UnitIndex>(SortedArray.ofSortedArray(_indices))
             return StructureElement.Loci(structure, [{ unit, indices }])
         }
     }
@@ -87,24 +93,25 @@ export function markPolymerElement(loci: Loci, structureGroup: StructureGroup, a
     if (!StructureElement.isLoci(loci)) return false
     const { structure, group } = structureGroup
     if (loci.structure !== structure) return false
-    const groupCount = group.units[0].polymerElements.length
+    const { polymerElements, model, elements } = group.units[0]
+    const { index, offsets } = model.atomicHierarchy.residueAtomSegments
+    const groupCount = polymerElements.length
     for (const e of loci.elements) {
         const unitIdx = group.unitIndexMap.get(e.unit.id)
         if (unitIdx !== undefined) {
-            if (Interval.is(e.indices)) {
-                const min = OrderedSet.indexOf(e.unit.polymerElements, e.unit.elements[Interval.min(e.indices)])
-                const max = OrderedSet.indexOf(e.unit.polymerElements, e.unit.elements[Interval.max(e.indices)])
-                if (min !== -1 && max !== -1) {
-                    if (apply(Interval.ofRange(unitIdx * groupCount + min, unitIdx * groupCount + max))) changed = true
+            // TODO optimized implementation for intervals
+            OrderedSet.forEach(e.indices, v => {
+                const rI = index[elements[v]]
+                const unitIndexBeg = OrderedSet.indexOf(elements, offsets[rI])
+                const unitIndexEnd = OrderedSet.indexOf(elements, offsets[rI + 1])
+                const unitIndexInterval = Interval.ofBounds(unitIndexBeg, unitIndexEnd)
+                if(!OrderedSet.isSubset(e.indices, unitIndexInterval)) return
+                const eI = getElementIndexForAtomRole(model, rI, 'trace')
+                const idx = OrderedSet.indexOf(e.unit.polymerElements, eI)
+                if (idx !== -1) {
+                    if (apply(Interval.ofSingleton(unitIdx * groupCount + idx))) changed = true
                 }
-            } else {
-                for (let i = 0, _i = e.indices.length; i < _i; i++) {
-                    const idx = OrderedSet.indexOf(e.unit.polymerElements, e.unit.elements[e.indices[i]])
-                    if (idx !== -1) {
-                        if (apply(Interval.ofSingleton(unitIdx * groupCount + idx))) changed = true
-                    }
-                }
-            }
+            })
         }
     }
     return changed