Browse Source

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

David Sehnal 6 years ago
parent
commit
e9180f7f95

+ 5 - 2
src/mol-data/int/sorted-ranges.ts

@@ -68,14 +68,17 @@ namespace SortedRanges {
         }
 
         constructor(private ranges: SortedRanges<T>, private set: OrderedSet<T>) {
-            if (ranges.length) {
+            // TODO cleanup, refactor to make it clearer
+            const min = SortedArray.findPredecessorIndex(this.ranges, OrderedSet.min(set))
+            const max = SortedArray.findPredecessorIndex(this.ranges, OrderedSet.max(set))
+            if (ranges.length && min !== max) {
                 this.curIndex = this.getRangeIndex(OrderedSet.min(set))
                 this.maxIndex = Math.min(ranges.length - 2, this.getRangeIndex(OrderedSet.max(set)))
                 this.curMin = this.ranges[this.curIndex]
                 this.updateInterval()
             }
 
-            this.hasNext = ranges.length > 0 && this.curIndex <= this.maxIndex
+            this.hasNext = ranges.length > 0 && min !== max && this.curIndex <= this.maxIndex
         }
     }
 }

+ 121 - 28
src/mol-geo/primitive/sheet.ts

@@ -7,6 +7,7 @@
 
 import { Vec3 } from 'mol-math/linear-algebra';
 import { ChunkedArray } from 'mol-data/util';
+import { MeshBuilderState } from '../shape/mesh-builder';
 
 const tA = Vec3.zero()
 const tB = Vec3.zero()
@@ -19,29 +20,39 @@ const positionVector = Vec3.zero()
 const normalVector = Vec3.zero()
 const torsionVector = Vec3.zero()
 
-export function addSheet(controlPoints: Helpers.NumberArray, normalVectors: Helpers.NumberArray, binormalVectors: Helpers.NumberArray, linearSegments: number, width: number, height: number, arrowWidth: number, vertices: ChunkedArray<number, 3>, normals: ChunkedArray<number, 3>, indices: ChunkedArray<number, 3>, ids: ChunkedArray<number, 1>, currentId: number) {
+const arrowVerticalVector = Vec3.zero()
+const p1 = Vec3.zero()
+const p2 = Vec3.zero()
+const p3 = Vec3.zero()
+const p4 = Vec3.zero()
+const p5 = Vec3.zero()
+const p6 = Vec3.zero()
+const p7 = Vec3.zero()
+const p8 = Vec3.zero()
 
-    const vertexCount = vertices.elementCount
+export function addSheet(controlPoints: Helpers.NumberArray, normalVectors: Helpers.NumberArray, binormalVectors: Helpers.NumberArray, linearSegments: number, width: number, height: number, arrowHeight: number, startCap: boolean, endCap: boolean, state: MeshBuilderState) {
+    const { vertices, normals, indices } = state
+
+    let vertexCount = vertices.elementCount
     let offsetLength = 0
 
-    if (arrowWidth > 0) {
+    if (arrowHeight > 0) {
         Vec3.fromArray(tA, controlPoints, 0)
         Vec3.fromArray(tB, controlPoints, linearSegments * 3)
-        offsetLength = arrowWidth / Vec3.magnitude(Vec3.sub(tV, tB, tA))
+        offsetLength = arrowHeight / Vec3.magnitude(Vec3.sub(tV, tB, tA))
     }
 
     for (let i = 0; i <= linearSegments; ++i) {
-        const actualWidth = arrowWidth === 0 ? width : arrowWidth * (1 - i / linearSegments);
-
+        const actualHeight = arrowHeight === 0 ? height : arrowHeight * (1 - i / linearSegments);
         const i3 = i * 3
 
         Vec3.fromArray(verticalVector, normalVectors, i3)
-        Vec3.scale(verticalVector, verticalVector, actualWidth);
+        Vec3.scale(verticalVector, verticalVector, actualHeight);
 
         Vec3.fromArray(horizontalVector, binormalVectors, i3)
-        Vec3.scale(horizontalVector, horizontalVector, height);
+        Vec3.scale(horizontalVector, horizontalVector, width);
 
-        if (arrowWidth > 0) {
+        if (arrowHeight > 0) {
             Vec3.fromArray(tA, normalVectors, i3)
             Vec3.fromArray(tB, binormalVectors, i3)
             Vec3.scale(normalOffset, Vec3.cross(normalOffset, tA, tB), offsetLength)
@@ -51,49 +62,41 @@ export function addSheet(controlPoints: Helpers.NumberArray, normalVectors: Help
         Vec3.fromArray(normalVector, normalVectors, i3)
         Vec3.fromArray(torsionVector, binormalVectors, i3)
 
-        Vec3.add(tA, Vec3.add(tA, Vec3.copy(tA, positionVector), horizontalVector), verticalVector)
+        Vec3.add(tA, Vec3.add(tA, positionVector, horizontalVector), verticalVector)
         Vec3.copy(tB, normalVector)
         ChunkedArray.add3(vertices, tA[0], tA[1], tA[2])
         ChunkedArray.add3(normals, tB[0], tB[1], tB[2])
-        ChunkedArray.add(ids, currentId);
 
-        Vec3.add(tA, Vec3.sub(tA, Vec3.copy(tA, positionVector), horizontalVector), verticalVector)
+        Vec3.add(tA, Vec3.sub(tA, positionVector, horizontalVector), verticalVector)
         ChunkedArray.add3(vertices, tA[0], tA[1], tA[2])
         ChunkedArray.add3(normals, tB[0], tB[1], tB[2])
-        ChunkedArray.add(ids, currentId);
 
-        Vec3.add(tA, Vec3.sub(tA, Vec3.copy(tA, positionVector), horizontalVector), verticalVector)
-        Vec3.add(tB, Vec3.scale(tB, Vec3.copy(tB, torsionVector), -1), normalOffset)
+        // Vec3.add(tA, Vec3.sub(tA, positionVector, horizontalVector), verticalVector) // reuse tA
+        Vec3.add(tB, Vec3.negate(tB, torsionVector), normalOffset)
         ChunkedArray.add3(vertices, tA[0], tA[1], tA[2])
         ChunkedArray.add3(normals, tB[0], tB[1], tB[2])
-        ChunkedArray.add(ids, currentId);
 
-        Vec3.sub(tA, Vec3.sub(tA, Vec3.copy(tA, positionVector), horizontalVector), verticalVector)
+        Vec3.sub(tA, Vec3.sub(tA, positionVector, horizontalVector), verticalVector)
         ChunkedArray.add3(vertices, tA[0], tA[1], tA[2])
         ChunkedArray.add3(normals, tB[0], tB[1], tB[2])
-        ChunkedArray.add(ids, currentId);
 
-        Vec3.sub(tA, Vec3.sub(tA, Vec3.copy(tA, positionVector), horizontalVector), verticalVector)
-        Vec3.scale(tB, Vec3.copy(tB, normalVector), -1)
+        // Vec3.sub(tA, Vec3.sub(tA, positionVector, horizontalVector), verticalVector) // reuse tA
+        Vec3.negate(tB, normalVector)
         ChunkedArray.add3(vertices, tA[0], tA[1], tA[2])
         ChunkedArray.add3(normals, tB[0], tB[1], tB[2])
-        ChunkedArray.add(ids, currentId);
 
-        Vec3.sub(tA, Vec3.add(tA, Vec3.copy(tA, positionVector), horizontalVector), verticalVector)
+        Vec3.sub(tA, Vec3.add(tA, positionVector, horizontalVector), verticalVector)
         ChunkedArray.add3(vertices, tA[0], tA[1], tA[2])
         ChunkedArray.add3(normals, tB[0], tB[1], tB[2])
-        ChunkedArray.add(ids, currentId);
 
-        Vec3.sub(tA, Vec3.add(tA, Vec3.copy(tA, positionVector), horizontalVector), verticalVector)
-        Vec3.add(tB, Vec3.copy(tB, torsionVector), normalOffset)
+        // Vec3.sub(tA, Vec3.add(tA, positionVector, horizontalVector), verticalVector) // reuse tA
+        Vec3.add(tB, torsionVector, normalOffset)
         ChunkedArray.add3(vertices, tA[0], tA[1], tA[2])
         ChunkedArray.add3(normals, tB[0], tB[1], tB[2])
-        ChunkedArray.add(ids, currentId);
 
-        Vec3.add(tA, Vec3.add(tA, Vec3.copy(tA, positionVector), horizontalVector), verticalVector)
+        Vec3.add(tA, Vec3.add(tA, positionVector, horizontalVector), verticalVector)
         ChunkedArray.add3(vertices, tA[0], tA[1], tA[2])
         ChunkedArray.add3(normals, tB[0], tB[1], tB[2])
-        ChunkedArray.add(ids, currentId);
     }
 
     for (let i = 0; i < linearSegments; ++i) {
@@ -112,4 +115,94 @@ export function addSheet(controlPoints: Helpers.NumberArray, normalVectors: Help
             );
         }
     }
+
+    if (startCap) {
+        const offset = 0
+        vertexCount = vertices.elementCount
+
+        Vec3.fromArray(verticalVector, normalVectors, offset)
+        Vec3.scale(verticalVector, verticalVector, height);
+
+        Vec3.fromArray(horizontalVector, binormalVectors, offset)
+        Vec3.scale(horizontalVector, horizontalVector, width);
+
+        Vec3.fromArray(positionVector, controlPoints, offset)
+
+        Vec3.add(p1, Vec3.add(p1, positionVector, horizontalVector), verticalVector)
+        Vec3.sub(p2, Vec3.add(p2, positionVector, horizontalVector), verticalVector)
+        Vec3.sub(p3, Vec3.sub(p3, positionVector, horizontalVector), verticalVector)
+        Vec3.add(p4, Vec3.sub(p4, positionVector, horizontalVector), verticalVector)
+
+        ChunkedArray.add3(vertices, p1[0], p1[1], p1[2])
+        ChunkedArray.add3(vertices, p2[0], p2[1], p2[2])
+        ChunkedArray.add3(vertices, p3[0], p3[1], p3[2])
+        ChunkedArray.add3(vertices, p4[0], p4[1], p4[2])
+
+        Vec3.cross(normalVector, horizontalVector, verticalVector)
+
+        if (arrowHeight === 0) {
+            for (let i = 0; i < 4; ++i) {
+                ChunkedArray.add3(normals, normalVector[0], normalVector[1], normalVector[2])
+            }
+
+            ChunkedArray.add3(indices, vertexCount + 2, vertexCount + 1, vertexCount);
+            ChunkedArray.add3(indices, vertexCount, vertexCount + 3, vertexCount + 2);
+        } else {
+            Vec3.fromArray(arrowVerticalVector, normalVectors, offset)
+            Vec3.scale(arrowVerticalVector, verticalVector, arrowHeight);
+
+            Vec3.add(p5, Vec3.add(p5, positionVector, horizontalVector), arrowVerticalVector)
+            Vec3.sub(p6, Vec3.add(p6, positionVector, horizontalVector), arrowVerticalVector)
+            Vec3.sub(p7, Vec3.sub(p7, positionVector, horizontalVector), arrowVerticalVector)
+            Vec3.add(p8, Vec3.sub(p8, positionVector, horizontalVector), arrowVerticalVector)
+
+            ChunkedArray.add3(vertices, p5[0], p5[1], p5[2])
+            ChunkedArray.add3(vertices, p6[0], p6[1], p6[2])
+            ChunkedArray.add3(vertices, p7[0], p7[1], p7[2])
+            ChunkedArray.add3(vertices, p8[0], p8[1], p8[2])
+
+            for (let i = 0; i < 8; ++i) {
+                ChunkedArray.add3(normals, normalVector[0], normalVector[1], normalVector[2])
+            }
+
+            ChunkedArray.add3(indices, vertexCount + 7, vertexCount, vertexCount + 4);
+            ChunkedArray.add3(indices, vertexCount + 7, vertexCount + 3, vertexCount);
+            ChunkedArray.add3(indices, vertexCount + 5, vertexCount + 1, vertexCount + 6);
+            ChunkedArray.add3(indices, vertexCount + 1, vertexCount + 2, vertexCount + 6);
+        }
+    }
+
+    if (endCap && arrowHeight === 0) {
+        const offset = linearSegments * 3
+        vertexCount = vertices.elementCount
+
+        Vec3.fromArray(verticalVector, normalVectors, offset)
+        Vec3.scale(verticalVector, verticalVector, height);
+
+        Vec3.fromArray(horizontalVector, binormalVectors, offset)
+        Vec3.scale(horizontalVector, horizontalVector, width);
+
+        Vec3.fromArray(positionVector, controlPoints, offset)
+
+        Vec3.add(p1, Vec3.add(p1, positionVector, horizontalVector), verticalVector)
+        Vec3.sub(p2, Vec3.add(p2, positionVector, horizontalVector), verticalVector)
+        Vec3.sub(p3, Vec3.sub(p3, positionVector, horizontalVector), verticalVector)
+        Vec3.add(p4, Vec3.sub(p4, positionVector, horizontalVector), verticalVector)
+
+        ChunkedArray.add3(vertices, p1[0], p1[1], p1[2])
+        ChunkedArray.add3(vertices, p2[0], p2[1], p2[2])
+        ChunkedArray.add3(vertices, p3[0], p3[1], p3[2])
+        ChunkedArray.add3(vertices, p4[0], p4[1], p4[2])
+
+        Vec3.cross(normalVector, horizontalVector, verticalVector)
+
+        for (let i = 0; i < 4; ++i) {
+            ChunkedArray.add3(normals, normalVector[0], normalVector[1], normalVector[2])
+        }
+
+        ChunkedArray.add3(indices, vertexCount + 2, vertexCount + 1, vertexCount);
+        ChunkedArray.add3(indices, vertexCount, vertexCount + 3, vertexCount + 2);
+    }
+
+    return (linearSegments + 1) * 8 + (startCap ? (arrowHeight === 0 ? 4 : 8) : 0) + (endCap && arrowHeight === 0 ? 4 : 0)
 }

+ 87 - 10
src/mol-geo/primitive/tube.ts

@@ -7,17 +7,21 @@
 
 import { Vec3 } from 'mol-math/linear-algebra';
 import { ChunkedArray } from 'mol-data/util';
+import { MeshBuilderState } from '../shape/mesh-builder';
 
 const normalVector = Vec3.zero()
 const binormalVector = Vec3.zero()
+const controlPoint = Vec3.zero()
 const tempPos = Vec3.zero()
 const a = Vec3.zero()
 const b = Vec3.zero()
 const u = Vec3.zero()
 const v = Vec3.zero()
 
-export function addTube(controlPoints: Helpers.NumberArray, normalVectors: Helpers.NumberArray, binormalVectors: Helpers.NumberArray, linearSegments: number, radialSegments: number, width: number, height: number, waveFactor: number, vertices: ChunkedArray<number, 3>, normals: ChunkedArray<number, 3>, indices: ChunkedArray<number, 3>, ids: ChunkedArray<number, 1>, currentId: number) {
-    const vertexCount = vertices.elementCount
+export function addTube(controlPoints: Helpers.NumberArray, normalVectors: Helpers.NumberArray, binormalVectors: Helpers.NumberArray, linearSegments: number, radialSegments: number, width: number, height: number, waveFactor: number, startCap: boolean, endCap: boolean, state: MeshBuilderState) {
+    const { vertices, normals, indices } = state
+
+    let vertexCount = vertices.elementCount
     const di = 1 / linearSegments
 
     for (let i = 0; i <= linearSegments; ++i) {
@@ -30,7 +34,7 @@ export function addTube(controlPoints: Helpers.NumberArray, normalVectors: Helpe
         const w = ff * width, h = ff * height;
 
         for (let j = 0; j < radialSegments; ++j) {
-            let t = 2 * Math.PI * j / radialSegments;
+            const t = 2 * Math.PI * j / radialSegments;
 
             Vec3.copy(a, u)
             Vec3.copy(b, v)
@@ -54,7 +58,6 @@ export function addTube(controlPoints: Helpers.NumberArray, normalVectors: Helpe
 
             ChunkedArray.add3(vertices, tempPos[0], tempPos[1], tempPos[2]);
             ChunkedArray.add3(normals, normalVector[0], normalVector[1], normalVector[2]);
-            ChunkedArray.add(ids, currentId);
         }
     }
 
@@ -62,16 +65,90 @@ export function addTube(controlPoints: Helpers.NumberArray, normalVectors: Helpe
         for (let j = 0; j < radialSegments; ++j) {
             ChunkedArray.add3(
                 indices,
-                (vertexCount + i * radialSegments + (j + 1) % radialSegments),
-                (vertexCount + (i + 1) * radialSegments + (j + 1) % radialSegments),
-                (vertexCount + i * radialSegments + j)
+                vertexCount + i * radialSegments + (j + 1) % radialSegments,
+                vertexCount + (i + 1) * radialSegments + (j + 1) % radialSegments,
+                vertexCount + i * radialSegments + j
             );
             ChunkedArray.add3(
                 indices,
-                (vertexCount + (i + 1) * radialSegments + (j + 1) % radialSegments),
-                (vertexCount + (i + 1) * radialSegments + j),
-                (vertexCount + i * radialSegments + j)
+                vertexCount + (i + 1) * radialSegments + (j + 1) % radialSegments,
+                vertexCount + (i + 1) * radialSegments + j,
+                vertexCount + i * radialSegments + j
             );
         }
     }
+
+    if (startCap) {
+        const offset = 0
+        vertexCount = vertices.elementCount
+        Vec3.fromArray(u, normalVectors, offset)
+        Vec3.fromArray(v, binormalVectors, offset)
+        Vec3.fromArray(controlPoint, controlPoints, offset)
+        Vec3.cross(normalVector, u, v)
+
+        ChunkedArray.add3(vertices, controlPoint[0], controlPoint[1], controlPoint[2]);
+        ChunkedArray.add3(normals, normalVector[0], normalVector[1], normalVector[2]);
+
+        for (let i = 0; i < radialSegments; ++i) {
+            const t = 2 * Math.PI * i / radialSegments;
+
+            Vec3.copy(a, u)
+            Vec3.copy(b, v)
+            Vec3.add(
+                tempPos,
+                Vec3.scale(a, a, height * Math.cos(t)),
+                Vec3.scale(b, b, width * Math.sin(t))
+            )
+            Vec3.add(tempPos, controlPoint, tempPos)
+
+            ChunkedArray.add3(vertices, tempPos[0], tempPos[1], tempPos[2]);
+            ChunkedArray.add3(normals, normalVector[0], normalVector[1], normalVector[2]);
+
+            ChunkedArray.add3(
+                indices,
+                vertexCount,
+                vertexCount + i + 1,
+                vertexCount + (i + 1) % radialSegments + 1
+            );
+        }
+    }
+
+    if (endCap) {
+        const offset = linearSegments * 3
+        vertexCount = vertices.elementCount
+        Vec3.fromArray(u, normalVectors, offset)
+        Vec3.fromArray(v, binormalVectors, offset)
+        Vec3.fromArray(controlPoint, controlPoints, offset)
+        Vec3.cross(normalVector, u, v)
+
+        ChunkedArray.add3(vertices, controlPoint[0], controlPoint[1], controlPoint[2]);
+        ChunkedArray.add3(normals, normalVector[0], normalVector[1], normalVector[2]);
+
+        for (let i = 0; i < radialSegments; ++i) {
+            const t = 2 * Math.PI * i / radialSegments;
+
+            Vec3.copy(a, u)
+            Vec3.copy(b, v)
+            Vec3.add(
+                tempPos,
+                Vec3.scale(a, a, height * Math.cos(t)),
+                Vec3.scale(b, b, width * Math.sin(t))
+            )
+            Vec3.add(tempPos, controlPoint, tempPos)
+
+            ChunkedArray.add3(vertices, tempPos[0], tempPos[1], tempPos[2]);
+            ChunkedArray.add3(normals, normalVector[0], normalVector[1], normalVector[2]);
+
+            if (i < radialSegments - 2) {
+                ChunkedArray.add3(
+                    indices,
+                    vertexCount + i + 1,
+                    vertexCount + (i + 1) % radialSegments + 1,
+                    vertexCount
+                );
+            }
+        }
+    }
+
+    return (linearSegments + 1) * radialSegments + (startCap ? radialSegments + 1 : 0) + (endCap ? radialSegments + 1 : 0)
 }

+ 14 - 5
src/mol-geo/representation/structure/cartoon.ts

@@ -8,45 +8,54 @@ import { StructureRepresentation, StructureUnitsRepresentation } from '.';
 import { PickingId } from '../../util/picking';
 import { Structure } from 'mol-model/structure';
 import { Task } from 'mol-task';
-import { Loci } from 'mol-model/loci';
+import { Loci, isEmptyLoci } from 'mol-model/loci';
 import { MarkerAction } from '../../util/marker-data';
 import { PolymerTraceVisual, DefaultPolymerTraceProps } from './visual/polymer-trace-mesh';
+import { PolymerGapVisual, DefaultPolymerGapProps } from './visual/polymer-gap-cylinder';
 
 export const DefaultCartoonProps = {
-    ...DefaultPolymerTraceProps
+    ...DefaultPolymerTraceProps,
+    ...DefaultPolymerGapProps
 }
 export type CartoonProps = Partial<typeof DefaultCartoonProps>
 
 export function CartoonRepresentation(): StructureRepresentation<CartoonProps> {
     const traceRepr = StructureUnitsRepresentation(PolymerTraceVisual)
+    const gapRepr = StructureUnitsRepresentation(PolymerGapVisual)
 
     return {
         get renderObjects() {
-            return [ ...traceRepr.renderObjects ]
+            return [ ...traceRepr.renderObjects, ...gapRepr.renderObjects ]
         },
         get props() {
-            return { ...traceRepr.props }
+            return { ...traceRepr.props, ...gapRepr.props }
         },
         create: (structure: Structure, props: CartoonProps = {} as CartoonProps) => {
             const p = Object.assign({}, DefaultCartoonProps, props)
             return Task.create('CartoonRepresentation', async ctx => {
                 await traceRepr.create(structure, p).runInContext(ctx)
+                await gapRepr.create(structure, p).runInContext(ctx)
             })
         },
         update: (props: CartoonProps) => {
             const p = Object.assign({}, props)
             return Task.create('Updating CartoonRepresentation', async ctx => {
                 await traceRepr.update(p).runInContext(ctx)
+                await gapRepr.update(p).runInContext(ctx)
             })
         },
         getLoci: (pickingId: PickingId) => {
-            return traceRepr.getLoci(pickingId)
+            const traceLoci = traceRepr.getLoci(pickingId)
+            const gapLoci = gapRepr.getLoci(pickingId)
+            return isEmptyLoci(traceLoci) ? gapLoci : traceLoci
         },
         mark: (loci: Loci, action: MarkerAction) => {
             traceRepr.mark(loci, action)
+            gapRepr.mark(loci, action)
         },
         destroy() {
             traceRepr.destroy()
+            gapRepr.destroy()
         }
     }
 }

+ 158 - 0
src/mol-geo/representation/structure/visual/polymer-gap-cylinder.ts

@@ -0,0 +1,158 @@
+/**
+ * Copyright (c) 2018 mol* contributors, licensed under MIT, See LICENSE file for more info.
+ *
+ * @author Alexander Rose <alexander.rose@weirdbyte.de>
+ */
+
+import { ValueCell } from 'mol-util/value-cell'
+
+import { createMeshRenderObject, MeshRenderObject } from 'mol-gl/render-object'
+import { Unit } from 'mol-model/structure';
+import { DefaultStructureProps, UnitsVisual } from '..';
+import { RuntimeContext } from 'mol-task'
+import { createTransforms, createColors } from './util/common';
+import { deepEqual } from 'mol-util';
+import { MeshValues } from 'mol-gl/renderable';
+import { getMeshData } from '../../../util/mesh-data';
+import { Mesh } from '../../../shape/mesh';
+import { PickingId } from '../../../util/picking';
+import { createMarkers, MarkerAction } from '../../../util/marker-data';
+import { Loci } from 'mol-model/loci';
+import { SizeTheme } from '../../../theme';
+import { createMeshValues, updateMeshValues, updateRenderableState, createRenderableState, DefaultMeshProps } from '../../util';
+import { MeshBuilder } from '../../../shape/mesh-builder';
+import { getPolymerGapCount, PolymerGapIterator } from './util/polymer';
+import { getElementLoci, markElement } from './util/element';
+import { Vec3 } from 'mol-math/linear-algebra';
+
+async function createPolymerGapCylinderMesh(ctx: RuntimeContext, unit: Unit, mesh?: Mesh) {
+    const polymerGapCount = getPolymerGapCount(unit)
+    if (!polymerGapCount) return Mesh.createEmpty(mesh)
+    console.log('polymerGapCount', polymerGapCount)
+
+    // TODO better vertex count estimates
+    const builder = MeshBuilder.create(polymerGapCount * 30, polymerGapCount * 30 / 2, mesh)
+
+    const { elements } = unit
+    const pos = unit.conformation.invariantPosition
+    const pA = Vec3.zero()
+    const pB = Vec3.zero()
+
+    let i = 0
+    const polymerGapIt = PolymerGapIterator(unit)
+    while (polymerGapIt.hasNext) {
+        // TODO size theme
+        const { centerA, centerB } = polymerGapIt.move()
+        if (centerA.element === centerB.element) {
+            builder.setId(centerA.element)
+            pos(elements[centerA.element], pA)
+            builder.addIcosahedron(pA, 0.6, 0)
+        } else {
+            pos(elements[centerA.element], pA)
+            pos(elements[centerB.element], pB)
+            builder.setId(centerA.element)
+            builder.addFixedCountDashedCylinder(pA, pB, 0.5, 10, { radiusTop: 0.2, radiusBottom: 0.2 })
+            builder.setId(centerB.element)
+            builder.addFixedCountDashedCylinder(pB, pA, 0.5, 10, { radiusTop: 0.2, radiusBottom: 0.2 })
+        }
+
+        if (i % 10000 === 0 && ctx.shouldUpdate) {
+            await ctx.update({ message: 'Gap mesh', current: i, max: polymerGapCount });
+        }
+        ++i
+    }
+
+    return builder.getMesh()
+}
+
+export const DefaultPolymerGapProps = {
+    ...DefaultMeshProps,
+    ...DefaultStructureProps,
+    sizeTheme: { name: 'physical', factor: 1 } as SizeTheme,
+    detail: 0,
+    unitKinds: [ Unit.Kind.Atomic, Unit.Kind.Spheres ] as Unit.Kind[]
+}
+export type PolymerGapProps = Partial<typeof DefaultPolymerGapProps>
+
+export function PolymerGapVisual(): UnitsVisual<PolymerGapProps> {
+    let renderObject: MeshRenderObject
+    let currentProps: typeof DefaultPolymerGapProps
+    let mesh: Mesh
+    let currentGroup: Unit.SymmetryGroup
+
+    return {
+        get renderObject () { return renderObject },
+        async create(ctx: RuntimeContext, group: Unit.SymmetryGroup, props: PolymerGapProps = {}) {
+            currentProps = Object.assign({}, DefaultPolymerGapProps, props)
+            currentGroup = group
+
+            const { colorTheme, unitKinds } = { ...DefaultPolymerGapProps, ...props }
+            const instanceCount = group.units.length
+            const elementCount = group.elements.length
+            const unit = group.units[0]
+
+            mesh = unitKinds.includes(unit.kind)
+                ? await createPolymerGapCylinderMesh(ctx, unit, mesh)
+                : Mesh.createEmpty(mesh)
+            // console.log(mesh)
+
+            const transforms = createTransforms(group)
+            const color = createColors(group, elementCount, colorTheme)
+            const marker = createMarkers(instanceCount * elementCount)
+
+            const counts = { drawCount: mesh.triangleCount * 3, elementCount, instanceCount }
+
+            const values: MeshValues = {
+                ...getMeshData(mesh),
+                ...color,
+                ...marker,
+                aTransform: transforms,
+                elements: mesh.indexBuffer,
+                ...createMeshValues(currentProps, counts),
+                aColor: ValueCell.create(new Float32Array(mesh.vertexCount * 3))
+            }
+            const state = createRenderableState(currentProps)
+
+            renderObject = createMeshRenderObject(values, state)
+        },
+        async update(ctx: RuntimeContext, props: PolymerGapProps) {
+            const newProps = Object.assign({}, currentProps, props)
+
+            if (!renderObject) return false
+
+            let updateColor = false
+
+            if (newProps.detail !== currentProps.detail) {
+                const unit = currentGroup.units[0]
+                mesh = await createPolymerGapCylinderMesh(ctx, unit, mesh)
+                ValueCell.update(renderObject.values.drawCount, mesh.triangleCount * 3)
+                updateColor = true
+            }
+
+            if (!deepEqual(newProps.colorTheme, currentProps.colorTheme)) {
+                updateColor = true
+            }
+
+            if (updateColor) {
+                const elementCount = currentGroup.elements.length
+                if (ctx.shouldUpdate) await ctx.update('Computing trace colors');
+                createColors(currentGroup, elementCount, newProps.colorTheme, renderObject.values)
+            }
+
+            updateMeshValues(renderObject.values, newProps)
+            updateRenderableState(renderObject.state, newProps)
+
+            currentProps = newProps
+            return true
+        },
+        getLoci(pickingId: PickingId) {
+            return getElementLoci(renderObject.id, currentGroup, pickingId)
+        },
+        mark(loci: Loci, action: MarkerAction) {
+            markElement(renderObject.values.tMarker, currentGroup, loci, action)
+        },
+        destroy() {
+            // TODO
+        }
+    }
+}

+ 4 - 4
src/mol-geo/representation/structure/visual/polymer-trace-mesh.ts

@@ -191,14 +191,14 @@ async function createPolymerTraceMesh(ctx: RuntimeContext, unit: Unit, mesh?: Me
 
         // TODO size theme
         if (SecondaryStructureType.is(v.secStrucType, SecondaryStructureType.Flag.Beta)) {
-            width = 1.0; height = 0.15
-            const arrowWidth = v.secStrucChange ? 1.7 : 0
-            builder.addSheet(controlPoints, normalVectors, binormalVectors, linearSegments, width, height, arrowWidth)
+            width = 0.15; height = 1.0
+            const arrowHeight = v.secStrucChange ? 1.7 : 0
+            builder.addSheet(controlPoints, normalVectors, binormalVectors, linearSegments, width, height, arrowHeight, true, true)
         } else {
             if (SecondaryStructureType.is(v.secStrucType, SecondaryStructureType.Flag.Helix)) {
                 width = 0.2; height = 1.0
             }
-            builder.addTube(controlPoints, normalVectors, binormalVectors, linearSegments, radialSegments, width, height, 1)
+            builder.addTube(controlPoints, normalVectors, binormalVectors, linearSegments, radialSegments, width, height, 1, true, true)
         }
 
         if (i % 10000 === 0 && ctx.shouldUpdate) {

+ 93 - 3
src/mol-geo/representation/structure/visual/util/polymer.ts

@@ -20,6 +20,14 @@ export function getPolymerRanges(unit: Unit): SortedRanges<ElementIndex> {
     }
 }
 
+export function getGapRanges(unit: Unit): SortedRanges<ElementIndex> {
+    switch (unit.kind) {
+        case Unit.Kind.Atomic: return unit.model.atomicHierarchy.gapRanges
+        case Unit.Kind.Spheres: return unit.model.coarseHierarchy.spheres.gapRanges
+        case Unit.Kind.Gaussians: return unit.model.coarseHierarchy.gaussians.gapRanges
+    }
+}
+
 export function getPolymerElementCount(unit: Unit) {
     let count = 0
     const { elements } = unit
@@ -48,6 +56,17 @@ export function getPolymerElementCount(unit: Unit) {
     return count
 }
 
+export function getPolymerGapCount(unit: Unit) {
+    let count = 0
+    const { elements } = unit
+    const gapIt = SortedRanges.transientSegments(getGapRanges(unit), elements)
+    while (gapIt.hasNext) {
+        const { start, end } = gapIt.move()
+        if (OrderedSet.areIntersecting(Interval.ofBounds(elements[start], elements[end - 1]), elements)) ++count
+    }
+    return count
+}
+
 function getResidueTypeAtomId(moleculeType: MoleculeType, atomType: 'trace' | 'direction') {
     switch (moleculeType) {
         case MoleculeType.protein:
@@ -134,9 +153,7 @@ export class AtomicPolymerBackboneIterator implements Iterator<PolymerBackbonePa
 
     private getElementIndex(residueIndex: ResidueIndex, atomType: 'trace' | 'direction') {
         const index = getElementIndexForResidueTypeAtomId(this.unit.model, residueIndex, atomType)
-        // // TODO handle case when it returns -1
-        // return SortedArray.indexOf(this.unit.elements, index) as ElementIndex
-
+        // TODO handle case when it returns -1
         const elementIndex = SortedArray.indexOf(this.unit.elements, index) as ElementIndex
         if (elementIndex === -1) {
             console.log('-1', residueIndex, atomType, index)
@@ -222,6 +239,79 @@ export class CoarsePolymerBackboneIterator implements Iterator<PolymerBackbonePa
     }
 }
 
+/** Iterates over gaps, i.e. the stem residues/coarse elements adjacent to gaps */
+export function PolymerGapIterator(unit: Unit): Iterator<PolymerGapPair> {
+    switch (unit.kind) {
+        case Unit.Kind.Atomic: return new AtomicPolymerGapIterator(unit)
+        case Unit.Kind.Spheres:
+        case Unit.Kind.Gaussians:
+            return new CoarsePolymerGapIterator(unit)
+    }
+}
+
+interface PolymerGapPair {
+    centerA: StructureElement
+    centerB: StructureElement
+}
+
+function createPolymerGapPair (unit: Unit) {
+    return {
+        centerA: StructureElement.create(unit),
+        centerB: StructureElement.create(unit),
+    }
+}
+
+export class AtomicPolymerGapIterator implements Iterator<PolymerGapPair> {
+    private value: PolymerGapPair
+    private gapIt: SortedRanges.Iterator<ElementIndex, ResidueIndex>
+    hasNext: boolean = false;
+
+    private getElementIndex(residueIndex: ResidueIndex, atomType: 'trace' | 'direction') {
+        const index = getElementIndexForResidueTypeAtomId(this.unit.model, residueIndex, atomType)
+        // TODO handle case when it returns -1
+        const elementIndex = SortedArray.indexOf(this.unit.elements, index) as ElementIndex
+        if (elementIndex === -1) {
+            console.log('-1', residueIndex, atomType, index)
+        }
+        return elementIndex === -1 ? 0 as ElementIndex : elementIndex
+    }
+
+    move() {
+        const { elements, residueIndex } = this.unit
+        const gapSegment = this.gapIt.move();
+        this.value.centerA.element = this.getElementIndex(residueIndex[elements[gapSegment.start]], 'trace')
+        this.value.centerB.element = this.getElementIndex(residueIndex[elements[gapSegment.end - 1]], 'trace')
+        this.hasNext = this.gapIt.hasNext
+        return this.value;
+    }
+
+    constructor(private unit: Unit.Atomic) {
+        this.gapIt = SortedRanges.transientSegments(getGapRanges(unit), unit.elements);
+        this.value = createPolymerGapPair(unit)
+        this.hasNext = this.gapIt.hasNext
+    }
+}
+
+export class CoarsePolymerGapIterator implements Iterator<PolymerGapPair> {
+    private value: PolymerGapPair
+    private gapIt: SortedRanges.Iterator<ElementIndex, ElementIndex>
+    hasNext: boolean = false;
+
+    move() {
+        const gapSegment = this.gapIt.move();
+        this.value.centerA.element = this.unit.elements[gapSegment.start]
+        this.value.centerB.element = this.unit.elements[gapSegment.end - 1]
+        this.hasNext = this.gapIt.hasNext
+        return this.value;
+    }
+
+    constructor(private unit: Unit.Spheres | Unit.Gaussians) {
+        this.gapIt = SortedRanges.transientSegments(getGapRanges(unit), unit.elements);
+        this.value = createPolymerGapPair(unit)
+        this.hasNext = this.gapIt.hasNext
+    }
+}
+
 /**
  * Iterates over individual residues/coarse elements in polymers of a unit while
  * providing information about the neighbourhood in the underlying model for drawing splines

+ 18 - 13
src/mol-geo/shape/mesh-builder.ts

@@ -16,12 +16,18 @@ import { getNormalMatrix } from '../util';
 import { addSheet } from '../primitive/sheet';
 import { addTube } from '../primitive/tube';
 
-type Primitive = {
+interface Primitive {
     vertices: Float32Array
     normals: Float32Array
     indices: Uint32Array
 }
 
+export interface MeshBuilderState {
+    vertices: ChunkedArray<number, 3>
+    normals: ChunkedArray<number, 3>
+    indices: ChunkedArray<number, 3>
+}
+
 export interface MeshBuilder {
     add(t: Mat4, _vertices: Float32Array, _normals: Float32Array, _indices?: Uint32Array): void
     addBox(t: Mat4, props?: BoxProps): void
@@ -29,8 +35,8 @@ export interface MeshBuilder {
     addDoubleCylinder(start: Vec3, end: Vec3, lengthScale: number, shift: Vec3, props: CylinderProps): void
     addFixedCountDashedCylinder(start: Vec3, end: Vec3, lengthScale: number, segmentCount: number, props: CylinderProps): void
     addIcosahedron(center: Vec3, radius: number, detail: number): void
-    addTube(controlPoints: Helpers.NumberArray, torsionVectors: Helpers.NumberArray, normalVectors: Helpers.NumberArray, linearSegments: number, radialSegments: number, width: number, height: number, waveFactor: number): void
-    addSheet(controlPoints: Helpers.NumberArray, normalVectors: Helpers.NumberArray, binormalVectors: Helpers.NumberArray, linearSegments: number, width: number, height: number, arrowWidth: number): void
+    addTube(centers: Helpers.NumberArray, normals: Helpers.NumberArray, binormals: Helpers.NumberArray, linearSegments: number, radialSegments: number, width: number, height: number, waveFactor: number, startCap: boolean, endCap: boolean): void
+    addSheet(centers: Helpers.NumberArray, normals: Helpers.NumberArray, binormals: Helpers.NumberArray, linearSegments: number, width: number, height: number, arrowHeight: number, startCap: boolean, endCap: boolean): void
     setId(id: number): void
     getMesh(): Mesh
 }
@@ -56,10 +62,7 @@ function setCylinderMat(m: Mat4, start: Vec3, dir: Vec3, length: number) {
     // direction so the triangles of adjacent cylinder will line up
     if (Vec3.dot(tmpCylinderMatDir, up) < 0) Vec3.scale(tmpCylinderMatDir, tmpCylinderMatDir, -1)
     Vec3.makeRotation(m, up, tmpCylinderMatDir)
-    // Mat4.fromTranslation(tmpCylinderMatTrans, tmpCylinderCenter)
-    // Mat4.mul(m, tmpCylinderMatTrans, m)
-    Mat4.setTranslation(m, tmpCylinderCenter)
-    return m
+    return Mat4.setTranslation(m, tmpCylinderCenter)
 }
 
 function getCylinder(props: CylinderProps) {
@@ -75,8 +78,7 @@ function getCylinder(props: CylinderProps) {
 const tmpIcosahedronMat = Mat4.identity()
 
 function setIcosahedronMat(m: Mat4, center: Vec3) {
-    Mat4.setTranslation(m, center)
-    return m
+    return Mat4.setTranslation(m, center)
 }
 
 function getIcosahedron(props: IcosahedronProps) {
@@ -96,6 +98,7 @@ export namespace MeshBuilder {
         const vertices = ChunkedArray.create(Float32Array, 3, chunkSize, mesh ? mesh.vertexBuffer.ref.value : initialCount);
         const normals = ChunkedArray.create(Float32Array, 3, chunkSize, mesh ? mesh.normalBuffer.ref.value : initialCount);
         const indices = ChunkedArray.create(Uint32Array, 3, chunkSize * 3, mesh ? mesh.indexBuffer.ref.value : initialCount * 3);
+        const state: MeshBuilderState = { vertices, normals, indices };
 
         const ids = ChunkedArray.create(Float32Array, 1, chunkSize, mesh ? mesh.idBuffer.ref.value : initialCount);
         const offsets = ChunkedArray.create(Uint32Array, 1, chunkSize, mesh ? mesh.offsetBuffer.ref.value : initialCount);
@@ -179,11 +182,13 @@ export namespace MeshBuilder {
                 setIcosahedronMat(tmpIcosahedronMat, center)
                 add(tmpIcosahedronMat, vertices, normals, indices)
             },
-            addTube: (controlPoints: Helpers.NumberArray, normalVectors: Helpers.NumberArray, binormalVectors: Helpers.NumberArray, linearSegments: number, radialSegments: number, width: number, height: number, waveFactor: number) => {
-                addTube(controlPoints, normalVectors, binormalVectors, linearSegments, radialSegments, width, height, waveFactor, vertices, normals, indices, ids, currentId)
+            addTube: (centers: Helpers.NumberArray, normals: Helpers.NumberArray, binormals: Helpers.NumberArray, linearSegments: number, radialSegments: number, width: number, height: number, waveFactor: number, startCap: boolean, endCap: boolean) => {
+                const addedVertexCount = addTube(centers, normals, binormals, linearSegments, radialSegments, width, height, waveFactor, startCap, endCap, state)
+                for (let i = 0, il = addedVertexCount; i < il; ++i) ChunkedArray.add(ids, currentId);
             },
-            addSheet: (controlPoints: Helpers.NumberArray, normalVectors: Helpers.NumberArray, binormalVectors: Helpers.NumberArray, linearSegments: number, width: number, height: number, arrowWidth: number) => {
-                addSheet(controlPoints, normalVectors, binormalVectors, linearSegments, width, height, arrowWidth, vertices, normals, indices, ids, currentId)
+            addSheet: (controls: Helpers.NumberArray, normals: Helpers.NumberArray, binormals: Helpers.NumberArray, linearSegments: number, width: number, height: number, arrowHeight: number, startCap: boolean, endCap: boolean) => {
+                const addedVertexCount = addSheet(controls, normals, binormals, linearSegments, width, height, arrowHeight, startCap, endCap, state)
+                for (let i = 0, il = addedVertexCount; i < il; ++i) ChunkedArray.add(ids, currentId);
             },
             setId: (id: number) => {
                 if (currentId !== id) {

+ 11 - 1
src/mol-math/linear-algebra/3d/vec3.ts

@@ -194,6 +194,16 @@ namespace Vec3 {
         return Vec3.scale(out, Vec3.normalize(out, a), l)
     }
 
+    /**
+     * Negates the components of a vec3
+     */
+    export function negate(out: Vec3, a: Vec3) {
+        out[0] = -a[0];
+        out[1] = -a[1];
+        out[2] = -a[2];
+        return out;
+    }
+
     /**
      * Returns the inverse of the components of a Vec3
      */
@@ -384,7 +394,7 @@ namespace Vec3 {
     export function exactEquals(a: Vec3, b: Vec3) {
         return a[0] === b[0] && a[1] === b[1] && a[2] === b[2];
     }
-    
+
     /**
      * Returns whether or not the vectors have approximately the same elements in the same position.
      */

+ 9 - 3
src/mol-model/structure/model/properties/utils/atomic-ranges.ts

@@ -12,6 +12,8 @@ import { ChemicalComponent } from '../chemical-component';
 import { MoleculeType, isPolymer } from '../../types';
 import { ElementIndex } from '../../indexing';
 
+// TODO add gaps at the ends of the chains by comparing to the polymer sequence data
+
 export function getAtomicRanges(data: AtomicData, segments: AtomicSegments, chemicalComponentMap: Map<string, ChemicalComponent>): AtomicRanges {
     const polymerRanges: number[] = []
     const gapRanges: number[] = []
@@ -20,6 +22,7 @@ export function getAtomicRanges(data: AtomicData, segments: AtomicSegments, chem
     const { label_seq_id, label_comp_id } = data.residues
 
     let prevSeqId: number
+    let prevStart: number
     let prevEnd: number
     let startIndex: number
 
@@ -27,6 +30,7 @@ export function getAtomicRanges(data: AtomicData, segments: AtomicSegments, chem
         const chainSegment = chainIt.move();
         residueIt.setSegment(chainSegment);
         prevSeqId = -1
+        prevStart = -1
         prevEnd = -1
         startIndex = -1
 
@@ -40,7 +44,7 @@ export function getAtomicRanges(data: AtomicData, segments: AtomicSegments, chem
                 if (startIndex !== -1) {
                     if (seqId !== prevSeqId + 1) {
                         polymerRanges.push(startIndex, prevEnd - 1)
-                        gapRanges.push(prevEnd - 1, residueSegment.start)
+                        gapRanges.push(prevStart, residueSegment.end - 1)
                         startIndex = residueSegment.start
                     } else if (!residueIt.hasNext) {
                         polymerRanges.push(startIndex, residueSegment.end - 1)
@@ -54,13 +58,15 @@ export function getAtomicRanges(data: AtomicData, segments: AtomicSegments, chem
                     startIndex = -1
                 }
             }
-            
+
+            prevStart = residueSegment.start
             prevEnd = residueSegment.end
             prevSeqId = seqId
         }
     }
 
-    console.log(polymerRanges, gapRanges)
+    console.log('polymerRanges', polymerRanges)
+    console.log('gapRanges', gapRanges)
 
     return {
         polymerRanges: SortedRanges.ofSortedRanges(polymerRanges as ElementIndex[]),

+ 2 - 0
src/mol-model/structure/model/properties/utils/coarse-ranges.ts

@@ -11,6 +11,7 @@ import { ChemicalComponent } from '../chemical-component';
 import { ElementIndex } from '../../indexing';
 
 // TODO assumes all coarse elements are part of a polymer
+// TODO add gaps at the ends of the chains by comparing to the polymer sequence data
 
 export function getCoarseRanges(data: CoarseElementData, chemicalComponentMap: Map<string, ChemicalComponent>): CoarseRanges {
     const polymerRanges: number[] = []
@@ -33,6 +34,7 @@ export function getCoarseRanges(data: CoarseElementData, chemicalComponentMap: M
             } else {
                 if (prevSeqEnd !== seq_id_begin.value(i) - 1) {
                     polymerRanges.push(startIndex, i - 1)
+                    gapRanges.push(i - 1, i)
                     startIndex = i
                 }
             }

+ 2 - 2
src/mol-view/stage.ts

@@ -80,9 +80,9 @@ export class Stage {
         // this.loadPdbid('3pqr') // inter unit bonds, two polymer chains, ligands, water
         // this.loadPdbid('4v5a') // ribosome
         // this.loadPdbid('3j3q') // ...
-        // this.loadPdbid('3sn6') // discontinuous chains
+        this.loadPdbid('3sn6') // discontinuous chains
         // this.loadMmcifUrl(`../../examples/1cbs_full.bcif`)
-        this.loadMmcifUrl(`../../examples/1cbs_updated.cif`)
+        // this.loadMmcifUrl(`../../examples/1cbs_updated.cif`)
         // this.loadMmcifUrl(`../../examples/1crn.cif`)
 
         // this.loadMmcifUrl(`../../../test/pdb-dev/PDBDEV_00000001.cif`) // ok