Browse Source

Merge branch 'master' into graphics

# Conflicts:
#	src/mol-gl/shader-code.ts
Alexander Rose 6 years ago
parent
commit
2a25e0abcd
38 changed files with 2476 additions and 1165 deletions
  1. 762 904
      package-lock.json
  2. 15 15
      package.json
  3. 1 1
      src/mol-geo/geometry/mesh/mesh-builder.ts
  4. 13 13
      src/mol-gl/shader-code.ts
  5. 6 2
      src/mol-gl/webgl/render-item.ts
  6. 15 15
      src/mol-io/reader/_spec/mol2.spec.ts
  7. 152 0
      src/mol-io/reader/_spec/ply.spec.ts
  8. 1 2
      src/mol-io/reader/cif/data-model.ts
  9. 3 10
      src/mol-io/reader/csv/data-model.ts
  10. 263 0
      src/mol-io/reader/ply/parser.ts
  11. 79 0
      src/mol-io/reader/ply/schema.ts
  12. 2 2
      src/mol-math/geometry/gaussian-density/gpu.ts
  13. 23 0
      src/mol-math/linear-algebra/3d/vec3.ts
  14. 14 0
      src/mol-math/linear-algebra/_spec/vec3.spec.ts
  15. 260 0
      src/mol-model-formats/shape/ply.ts
  16. 26 0
      src/mol-model-formats/structure/_spec/pdb.spec.ts
  17. 40 4
      src/mol-model-formats/structure/pdb/to-cif.ts
  18. 16 0
      src/mol-model/shape/provider.ts
  19. 4 1
      src/mol-model/shape/shape.ts
  20. 6 1
      src/mol-model/structure/model/properties/seconday-structure.ts
  21. 1 1
      src/mol-model/structure/model/properties/utils/guess-element.ts
  22. 494 105
      src/mol-model/structure/model/properties/utils/secondary-structure.ts
  23. 75 0
      src/mol-model/structure/model/properties/utils/secondary-structure.validation
  24. 32 31
      src/mol-model/structure/model/types.ts
  25. 1 1
      src/mol-plugin/behavior/dynamic/labels.ts
  26. 2 0
      src/mol-plugin/state/actions/data-format.ts
  27. 31 0
      src/mol-plugin/state/actions/shape.ts
  28. 10 0
      src/mol-plugin/state/objects.ts
  29. 18 0
      src/mol-plugin/state/transforms/data.ts
  30. 22 2
      src/mol-plugin/state/transforms/model.ts
  31. 35 1
      src/mol-plugin/state/transforms/representation.ts
  32. 1 1
      src/mol-plugin/util/structure-labels.ts
  33. 1 3
      src/mol-repr/shape/representation.ts
  34. 5 1
      src/mol-theme/color/secondary-structure.ts
  35. 6 6
      src/mol-util/array.ts
  36. 1 1
      src/mol-util/param-definition.ts
  37. 34 35
      src/tests/browser/index.html
  38. 6 7
      src/tests/browser/render-shape.ts

File diff suppressed because it is too large
+ 762 - 904
package-lock.json


+ 15 - 15
package.json

@@ -79,18 +79,18 @@
     "@types/benchmark": "^1.0.31",
     "@types/compression": "0.0.36",
     "@types/express": "^4.16.1",
-    "@types/jest": "^24.0.9",
-    "@types/node": "^11.10.4",
-    "@types/node-fetch": "^2.1.6",
-    "@types/react": "^16.8.6",
-    "@types/react-dom": "^16.8.2",
+    "@types/jest": "^24.0.11",
+    "@types/node": "^11.11.6",
+    "@types/node-fetch": "^2.1.7",
+    "@types/react": "^16.8.8",
+    "@types/react-dom": "^16.8.3",
     "@types/webgl2": "0.0.4",
     "@types/swagger-ui-dist": "3.0.0",
     "benchmark": "^2.1.4",
     "circular-dependency-plugin": "^5.0.2",
     "concurrently": "^4.1.0",
     "cpx": "^1.5.0",
-    "css-loader": "^2.1.0",
+    "css-loader": "^2.1.1",
     "extra-watch-webpack-plugin": "^1.0.3",
     "file-loader": "^3.0.1",
     "glslify": "^7.0.0",
@@ -99,31 +99,31 @@
     "graphql-code-generator": "^0.18.0",
     "graphql-codegen-time": "^0.18.0",
     "graphql-codegen-typescript-template": "^0.18.0",
-    "jest": "^24.1.0",
+    "jest": "^24.5.0",
     "jest-raw-loader": "^1.0.1",
     "mini-css-extract-plugin": "^0.5.0",
     "node-sass": "^4.11.0",
-    "raw-loader": "^1.0.0",
+    "raw-loader": "^2.0.0",
     "resolve-url-loader": "^3.0.1",
     "sass-loader": "^7.1.0",
     "style-loader": "^0.23.1",
     "ts-jest": "^24.0.0",
-    "tslint": "^5.13.1",
-    "typescript": "^3.3.3",
-    "uglify-js": "^3.4.9",
+    "tslint": "^5.14.0",
+    "typescript": "^3.3.4000",
+    "uglify-js": "^3.5.1",
     "util.promisify": "^1.0.0",
     "webpack": "^4.29.6",
-    "webpack-cli": "^3.2.3"
+    "webpack-cli": "^3.3.0"
   },
   "dependencies": {
     "argparse": "^1.0.10",
-    "compression": "^1.7.3",
+    "compression": "^1.7.4",
     "express": "^4.16.4",
     "graphql": "^14.1.1",
     "immutable": "^3.8.2",
     "node-fetch": "^2.3.0",
-    "react": "^16.8.4",
-    "react-dom": "^16.8.4",
+    "react": "^16.8.5",
+    "react-dom": "^16.8.5",
     "rxjs": "^6.4.0",
     "swagger-ui-dist": "^3.21.0"
   }

+ 1 - 1
src/mol-geo/geometry/mesh/mesh-builder.ts

@@ -45,7 +45,7 @@ export namespace MeshBuilder {
     export function addTriangle(state: State, a: Vec3, b: Vec3, c: Vec3) {
         const { vertices, normals, indices, groups, currentGroup } = state
         const offset = vertices.elementCount
-        
+
         // positions
         ChunkedArray.add3(vertices, a[0], a[1], a[2]);
         ChunkedArray.add3(vertices, b[0], b[1], b[2]);

+ 13 - 13
src/mol-gl/shader-code.ts

@@ -1,5 +1,5 @@
 /**
- * Copyright (c) 2018 mol* contributors, licensed under MIT, See LICENSE file for more info.
+ * Copyright (c) 2018-2019 mol* contributors, licensed under MIT, See LICENSE file for more info.
  *
  * @author Alexander Rose <alexander.rose@weirdbyte.de>
  */
@@ -31,38 +31,38 @@ export function ShaderCode(vert: string, frag: string, extensions: ShaderExtensi
 }
 
 export const PointsShaderCode = ShaderCode(
-    require('mol-gl/shader/points.vert'),
-    require('mol-gl/shader/points.frag'),
+    require('mol-gl/shader/points.vert').default,
+    require('mol-gl/shader/points.frag').default,
     { standardDerivatives: false, fragDepth: false }
 )
 
 export const SpheresShaderCode = ShaderCode(
-    require('mol-gl/shader/spheres.vert'),
-    require('mol-gl/shader/spheres.frag'),
+    require('mol-gl/shader/spheres.vert').default,
+    require('mol-gl/shader/spheres.frag').default,
     { standardDerivatives: false, fragDepth: true }
 )
 
 export const TextShaderCode = ShaderCode(
-    require('mol-gl/shader/text.vert'),
-    require('mol-gl/shader/text.frag'),
+    require('mol-gl/shader/text.vert').default,
+    require('mol-gl/shader/text.frag').default,
     { standardDerivatives: true, fragDepth: false }
 )
 
 export const LinesShaderCode = ShaderCode(
-    require('mol-gl/shader/lines.vert'),
-    require('mol-gl/shader/lines.frag'),
+    require('mol-gl/shader/lines.vert').default,
+    require('mol-gl/shader/lines.frag').default,
     { standardDerivatives: false, fragDepth: false }
 )
 
 export const MeshShaderCode = ShaderCode(
-    require('mol-gl/shader/mesh.vert'),
-    require('mol-gl/shader/mesh.frag'),
+    require('mol-gl/shader/mesh.vert').default,
+    require('mol-gl/shader/mesh.frag').default,
     { standardDerivatives: true, fragDepth: false }
 )
 
 export const DirectVolumeShaderCode = ShaderCode(
-    require('mol-gl/shader/direct-volume.vert'),
-    require('mol-gl/shader/direct-volume.frag'),
+    require('mol-gl/shader/direct-volume.vert').default,
+    require('mol-gl/shader/direct-volume.frag').default,
     { standardDerivatives: false, fragDepth: true }
 )
 

+ 6 - 2
src/mol-gl/webgl/render-item.ts

@@ -131,6 +131,7 @@ export function createRenderItem(ctx: WebGLContext, drawMode: DrawMode, shaderCo
     const valueChanges = createValueChanges()
 
     let destroyed = false
+    let currentProgramId = -1
 
     return {
         id,
@@ -142,9 +143,12 @@ export function createRenderItem(ctx: WebGLContext, drawMode: DrawMode, shaderCo
             const program = programs[variant].value
             const vertexArray = vertexArrays[variant]
             program.setUniforms(uniformValueEntries)
-            if (materialId === -1 || materialId !== ctx.currentMaterialId) {
-                // console.log('materialId changed or -1', materialId)
+            if (program.id !== currentProgramId ||
+                materialId === -1 || materialId !== ctx.currentMaterialId
+            ) {
+                // console.log('program.id changed or materialId changed/-1', materialId)
                 program.setUniforms(materialUniformValueEntries)
+                currentProgramId = program.id
                 ctx.currentMaterialId = materialId
             }
             program.bindTextures(textures)

+ 15 - 15
src/mol-io/reader/_spec/mol2.spec.ts

@@ -265,10 +265,10 @@ describe('mol2 reader', () => {
         expect(molecule.num_subst).toBe(0);
         expect(molecule.num_feat).toBe(0);
         expect(molecule.num_sets).toBe(0);
-        expect(molecule.mol_type).toBe("SMALL")
-        expect(molecule.charge_type).toBe("GASTEIGER");
-        expect(molecule.status_bits).toBe("");
-        expect(molecule.mol_comment).toBe("");
+        expect(molecule.mol_type).toBe('SMALL')
+        expect(molecule.charge_type).toBe('GASTEIGER');
+        expect(molecule.status_bits).toBe('');
+        expect(molecule.mol_comment).toBe('');
 
         // required atom fields
         expect(atoms.count).toBe(26);
@@ -277,7 +277,7 @@ describe('mol2 reader', () => {
         expect(atoms.x.value(0)).toBeCloseTo(1.7394, 0.001);
         expect(atoms.y.value(0)).toBeCloseTo(-2.1169, 0.0001);
         expect(atoms.z.value(0)).toBeCloseTo(-1.0893, 0.0001);
-        expect(atoms.atom_type.value(0)).toBe("O.3");
+        expect(atoms.atom_type.value(0)).toBe('O.3');
 
         // optional atom fields
         expect(atoms.subst_id.value(0)).toBe(1);
@@ -316,10 +316,10 @@ describe('mol2 reader', () => {
         expect(molecule.num_subst).toBe(0);
         expect(molecule.num_feat).toBe(0);
         expect(molecule.num_sets).toBe(0);
-        expect(molecule.mol_type).toBe("SMALL")
-        expect(molecule.charge_type).toBe("GASTEIGER");
-        expect(molecule.status_bits).toBe("");
-        expect(molecule.mol_comment).toBe("");
+        expect(molecule.mol_type).toBe('SMALL')
+        expect(molecule.charge_type).toBe('GASTEIGER');
+        expect(molecule.status_bits).toBe('');
+        expect(molecule.mol_comment).toBe('');
 
         // required atom fields
         expect(atoms.count).toBe(26);
@@ -328,7 +328,7 @@ describe('mol2 reader', () => {
         expect(atoms.x.value(0)).toBeCloseTo(1.7394, 0.001);
         expect(atoms.y.value(0)).toBeCloseTo(-2.1169, 0.0001);
         expect(atoms.z.value(0)).toBeCloseTo(-1.0893, 0.0001);
-        expect(atoms.atom_type.value(0)).toBe("O.3");
+        expect(atoms.atom_type.value(0)).toBe('O.3');
 
         // optional atom fields
         expect(atoms.subst_id.value(0)).toBe(1);
@@ -367,10 +367,10 @@ describe('mol2 reader', () => {
         expect(molecule.num_subst).toBe(0);
         expect(molecule.num_feat).toBe(0);
         expect(molecule.num_sets).toBe(0);
-        expect(molecule.mol_type).toBe("SMALL")
-        expect(molecule.charge_type).toBe("GASTEIGER");
-        expect(molecule.status_bits).toBe("");
-        expect(molecule.mol_comment).toBe("");
+        expect(molecule.mol_type).toBe('SMALL')
+        expect(molecule.charge_type).toBe('GASTEIGER');
+        expect(molecule.status_bits).toBe('');
+        expect(molecule.mol_comment).toBe('');
 
         // required atom fields
         expect(atoms.count).toBe(26);
@@ -379,7 +379,7 @@ describe('mol2 reader', () => {
         expect(atoms.x.value(0)).toBeCloseTo(1.7394, 0.001);
         expect(atoms.y.value(0)).toBeCloseTo(-2.1169, 0.0001);
         expect(atoms.z.value(0)).toBeCloseTo(-1.0893, 0.0001);
-        expect(atoms.atom_type.value(0)).toBe("O.3");
+        expect(atoms.atom_type.value(0)).toBe('O.3');
 
         // optional atom fields
         expect(atoms.subst_id.value(0)).toBe(0);

+ 152 - 0
src/mol-io/reader/_spec/ply.spec.ts

@@ -0,0 +1,152 @@
+/**
+ * Copyright (c) 2019 mol* contributors, licensed under MIT, See LICENSE file for more info.
+ *
+ * @author Alexander Rose <alexander.rose@weirdbyte.de>
+ */
+
+import Ply from '../ply/parser'
+import { PlyTable, PlyList } from '../ply/schema';
+
+const plyString = `ply
+format ascii 1.0
+comment file created by MegaMol
+element vertex 6
+property float x
+property float y
+property float z
+property uchar red
+property uchar green
+property uchar blue
+property uchar alpha
+property float nx
+property float ny
+property float nz
+property int atomid
+property uchar contactcount_r
+property uchar contactcount_g
+property uchar contactcount_b
+property uchar contactsteps_r
+property uchar contactsteps_g
+property uchar contactsteps_b
+property uchar hbonds_r
+property uchar hbonds_g
+property uchar hbonds_b
+property uchar hbondsteps_r
+property uchar hbondsteps_g
+property uchar hbondsteps_b
+property uchar molcount_r
+property uchar molcount_g
+property uchar molcount_b
+property uchar spots_r
+property uchar spots_g
+property uchar spots_b
+property uchar rmsf_r
+property uchar rmsf_g
+property uchar rmsf_b
+element face 2
+property list uchar int vertex_index
+end_header
+130.901 160.016 163.033 90 159 210 255 -0.382 -0.895 -0.231 181 21 100 150 24 102 151 20 100 150 20 100 150 30 106 154 20 100 150 171 196 212
+131.372 159.778 162.83 90 159 210 255 -0.618 -0.776 -0.129 178 21 100 150 24 102 151 20 100 150 20 100 150 30 106 154 20 100 150 141 177 199
+131.682 159.385 163.089 90 159 210 255 -0.773 -0.579 -0.259 180 21 100 150 24 102 151 20 100 150 20 100 150 30 106 154 20 100 150 172 196 212
+131.233 160.386 162.11 90 159 210 255 -0.708 -0.383 -0.594 178 21 100 150 24 102 151 20 100 150 20 100 150 30 106 154 20 100 150 141 177 199
+130.782 160.539 162.415 90 159 210 255 -0.482 -0.459 -0.746 181 21 100 150 24 102 151 20 100 150 20 100 150 30 106 154 20 100 150 171 196 212
+131.482 160.483 161.621 90 159 210 255 -0.832 -0.431 -0.349 179 21 100 150 24 102 151 20 100 150 20 100 150 30 106 154 20 100 150 171 196 212
+3 0 2 1
+3 3 5 4
+`
+
+const plyCubeString = `ply
+format ascii 1.0
+comment test cube
+element vertex 24
+property float32 x
+property float32 y
+property float32 z
+property uint32 material_index
+element face 6
+property list uint8 int32 vertex_indices
+element material 6
+property uint8 red
+property uint8 green
+property uint8 blue
+end_header
+-1 -1 -1 0
+1 -1 -1 0
+1 1 -1 0
+-1 1 -1 0
+1 -1 1 1
+-1 -1 1 1
+-1 1 1 1
+1 1 1 1
+1 1 1 2
+1 1 -1 2
+1 -1 -1 2
+1 -1 1 2
+-1 1 -1 3
+-1 1 1 3
+-1 -1 1 3
+-1 -1 -1 3
+-1 1 1 4
+-1 1 -1 4
+1 1 -1 4
+1 1 1 4
+1 -1 1 5
+1 -1 -1 5
+-1 -1 -1 5
+-1 -1 1 5
+4 0 1 2 3
+4 4 5 6 7
+4 8 9 10 11
+4 12 13 14 15
+4 16 17 18 19
+4 20 21 22 23
+255 0 0
+0 255 0
+0 0 255
+255 255 0
+0 255 255
+255 0 255
+`
+
+
+describe('ply reader', () => {
+    it('basic', async () => {
+        const parsed = await Ply(plyString).run();
+        if (parsed.isError) return;
+        const plyFile = parsed.result;
+
+        const vertex = plyFile.getElement('vertex') as PlyTable
+        if (!vertex) return
+        const x = vertex.getProperty('x')
+        if (!x) return
+        expect(x.value(0)).toEqual(130.901)
+
+        const face = plyFile.getElement('face') as PlyList
+        if (!face) return
+        expect(face.value(0)).toEqual({ count: 3, entries: [0, 2, 1]})
+        expect(face.value(1)).toEqual({ count: 3, entries: [3, 5, 4]})
+
+        expect.assertions(3)
+    });
+
+    it('material', async () => {
+        const parsed = await Ply(plyCubeString).run();
+        if (parsed.isError) return;
+        const plyFile = parsed.result;
+
+        const vertex = plyFile.getElement('vertex') as PlyTable
+        if (!vertex) return
+        expect(vertex.rowCount).toBe(24)
+
+        const face = plyFile.getElement('face') as PlyList
+        if (!face) return
+        expect(face.rowCount).toBe(6)
+
+        const material = plyFile.getElement('face') as PlyTable
+        if (!material) return
+        expect(face.rowCount).toBe(6)
+
+        expect.assertions(3)
+    });
+});

+ 1 - 2
src/mol-io/reader/cif/data-model.ts

@@ -199,7 +199,7 @@ export namespace CifField {
 
     export function ofColumn(column: Column<any>): CifField {
         const { rowCount, valueKind, areValuesEqual } = column;
-        
+
         let str: CifField['str']
         let int: CifField['int']
         let float: CifField['float']
@@ -219,7 +219,6 @@ export namespace CifField {
             default:
                 throw new Error('unsupported')
         }
-                
 
         return {
             __array: void 0,

+ 3 - 10
src/mol-io/reader/csv/data-model.ts

@@ -9,12 +9,11 @@ import { CifField as CsvColumn } from '../cif/data-model'
 export { CsvColumn }
 
 export interface CsvFile {
-    readonly name?: string,
     readonly table: CsvTable
 }
 
-export function CsvFile(table: CsvTable, name?: string): CsvFile {
-    return { name, table };
+export function CsvFile(table: CsvTable): CsvFile {
+    return { table };
 }
 
 export interface CsvTable {
@@ -27,10 +26,4 @@ export function CsvTable(rowCount: number, columnNames: string[], columns: CsvCo
     return { rowCount, columnNames: [...columnNames], getColumn(name) { return columns[name]; } };
 }
 
-export type CsvColumns = { [name: string]: CsvColumn }
-
-// export namespace CsvTable {
-//     export function empty(name: string): Table {
-//         return { rowCount: 0, name, fieldNames: [], getColumn(name: string) { return void 0; } };
-//     };
-// }
+export type CsvColumns = { [name: string]: CsvColumn }

+ 263 - 0
src/mol-io/reader/ply/parser.ts

@@ -0,0 +1,263 @@
+/**
+ * Copyright (c) 2019 mol* contributors, licensed under MIT, See LICENSE file for more info.
+ *
+ * @author Alexander Rose <alexander.rose@weirdbyte.de>
+ */
+
+import { ReaderResult as Result } from '../result'
+import { Task, RuntimeContext } from 'mol-task'
+import { PlyFile, PlyType, PlyElement } from './schema';
+import { Tokenizer, TokenBuilder, Tokens } from '../common/text/tokenizer';
+import { Column } from 'mol-data/db';
+import { TokenColumn } from '../common/text/column/token';
+
+interface State {
+    data: string
+    tokenizer: Tokenizer
+    runtimeCtx: RuntimeContext
+
+    comments: string[]
+    elementSpecs: ElementSpec[]
+    elements: PlyElement[]
+}
+
+function State(data: string, runtimeCtx: RuntimeContext): State {
+    const tokenizer = Tokenizer(data)
+    return {
+        data,
+        tokenizer,
+        runtimeCtx,
+
+        comments: [],
+        elementSpecs: [],
+        elements: []
+    }
+}
+
+type ColumnProperty = { kind: 'column', type: PlyType, name: string }
+type ListProperty = { kind: 'list', countType: PlyType, dataType: PlyType, name: string }
+type Property = ColumnProperty | ListProperty
+
+type TableElementSpec = { kind: 'table', name: string, count: number, properties: ColumnProperty[] }
+type ListElementSpec = { kind: 'list', name: string, count: number, property: ListProperty }
+type ElementSpec = TableElementSpec | ListElementSpec
+
+function markHeader(tokenizer: Tokenizer) {
+    const endHeaderIndex = tokenizer.data.indexOf('end_header', tokenizer.position)
+    if (endHeaderIndex === -1) throw new Error(`no 'end_header' record found`)
+    // TODO set `tokenizer.lineNumber` correctly
+    tokenizer.tokenStart = tokenizer.position
+    tokenizer.tokenEnd = endHeaderIndex
+    tokenizer.position = endHeaderIndex
+    Tokenizer.eatLine(tokenizer)
+}
+
+function parseHeader(state: State) {
+    const { tokenizer, comments, elementSpecs } = state
+
+    markHeader(tokenizer)
+    const headerLines = Tokenizer.getTokenString(tokenizer).split(/\r?\n/)
+
+    if (headerLines[0] !== 'ply') throw new Error(`data not starting with 'ply'`)
+    if (headerLines[1] !== 'format ascii 1.0') throw new Error(`format not 'ascii 1.0'`)
+
+    let currentName: string | undefined
+    let currentCount: number | undefined
+    let currentProperties: Property[] | undefined
+
+
+    function addCurrentElementSchema() {
+        if (currentName !== undefined && currentCount !== undefined && currentProperties !== undefined) {
+            let isList = false
+            for (let i = 0, il = currentProperties.length; i < il; ++i) {
+                const p = currentProperties[i]
+                if (p.kind === 'list') {
+                    isList = true
+                    break
+                }
+            }
+            if (isList && currentProperties.length !== 1) throw new Error('expected single list property')
+            if (isList) {
+                elementSpecs.push({
+                    kind: 'list',
+                    name: currentName,
+                    count: currentCount,
+                    property: currentProperties[0] as ListProperty
+                })
+            } else {
+                elementSpecs.push({
+                    kind: 'table',
+                    name: currentName,
+                    count: currentCount,
+                    properties: currentProperties as ColumnProperty[]
+                })
+            }
+        }
+    }
+
+    for (let i = 2, il = headerLines.length; i < il; ++i) {
+        const l = headerLines[i]
+        const ls = l.split(' ')
+        if (l.startsWith('comment')) {
+            comments.push(l.substr(8))
+        } else if (l.startsWith('element')) {
+            addCurrentElementSchema()
+            currentProperties = []
+            currentName = ls[1]
+            currentCount = parseInt(ls[2])
+        } else if (l.startsWith('property')) {
+            if (currentProperties === undefined) throw new Error(`properties outside of element`)
+            if (ls[1] === 'list') {
+                currentProperties.push({
+                    kind: 'list',
+                    countType: PlyType(ls[2]),
+                    dataType: PlyType(ls[3]),
+                    name: ls[4]
+                })
+            } else {
+                currentProperties.push({
+                    kind: 'column',
+                    type: PlyType(ls[1]),
+                    name: ls[2]
+                })
+            }
+        } else if (l.startsWith('end_header')) {
+            addCurrentElementSchema()
+        } else {
+            console.warn('unknown header line')
+        }
+    }
+}
+
+function parseElements(state: State) {
+    const { elementSpecs } = state
+    for (let i = 0, il = elementSpecs.length; i < il; ++i) {
+        const spec = elementSpecs[i]
+        if (spec.kind === 'table') parseTableElement(state, spec)
+        else if (spec.kind === 'list') parseListElement(state, spec)
+    }
+}
+
+function getColumnSchema(type: PlyType): Column.Schema {
+    switch (type) {
+        case 'char': case 'uchar': case 'int8': case 'uint8':
+        case 'short': case 'ushort': case 'int16': case 'uint16':
+        case 'int': case 'uint': case 'int32': case 'uint32':
+            return Column.Schema.int
+        case 'float': case 'double': case 'float32': case 'float64':
+            return Column.Schema.float
+    }
+}
+
+function parseTableElement(state: State, spec: TableElementSpec) {
+    const { elements, tokenizer } = state
+    const { count, properties } = spec
+    const propertyCount = properties.length
+    const propertyNames: string[] = []
+    const propertyTypes: PlyType[] = []
+    const propertyTokens: Tokens[] = []
+    const propertyColumns = new Map<string, Column<number>>()
+
+    for (let i = 0, il = propertyCount; i < il; ++i) {
+        const tokens = TokenBuilder.create(tokenizer.data, count * 2)
+        propertyTokens.push(tokens)
+    }
+
+    for (let i = 0, il = count; i < il; ++i) {
+        for (let j = 0, jl = propertyCount; j < jl; ++j) {
+            Tokenizer.skipWhitespace(tokenizer)
+            Tokenizer.markStart(tokenizer)
+            Tokenizer.eatValue(tokenizer)
+            TokenBuilder.addUnchecked(propertyTokens[j], tokenizer.tokenStart, tokenizer.tokenEnd)
+        }
+    }
+
+    for (let i = 0, il = propertyCount; i < il; ++i) {
+        const { type, name } = properties[i]
+        const column = TokenColumn(propertyTokens[i], getColumnSchema(type))
+        propertyNames.push(name)
+        propertyTypes.push(type)
+        propertyColumns.set(name, column)
+    }
+
+    elements.push({
+        kind: 'table',
+        rowCount: count,
+        propertyNames,
+        propertyTypes,
+        getProperty: (name: string) => propertyColumns.get(name)
+    })
+}
+
+function parseListElement(state: State, spec: ListElementSpec) {
+    const { elements, tokenizer } = state
+    const { count, property } = spec
+
+    // initial tokens size assumes triangle index data
+    const tokens = TokenBuilder.create(tokenizer.data, count * 2 * 3)
+
+    const offsets = new Uint32Array(count + 1)
+    let entryCount = 0
+
+    for (let i = 0, il = count; i < il; ++i) {
+        // skip over row entry count as it is determined by line break
+        Tokenizer.skipWhitespace(tokenizer)
+        Tokenizer.eatValue(tokenizer)
+
+        while (Tokenizer.skipWhitespace(tokenizer) !== 10) {
+            ++entryCount
+            Tokenizer.markStart(tokenizer)
+            Tokenizer.eatValue(tokenizer)
+            TokenBuilder.addToken(tokens, tokenizer)
+        }
+        offsets[i + 1] = entryCount
+    }
+
+    // console.log(tokens.indices)
+    // console.log(offsets)
+
+    /** holds row value entries transiently */
+    const listValue = {
+        entries: [] as number[],
+        count: 0
+    }
+
+    const column = TokenColumn(tokens, getColumnSchema(property.dataType))
+
+    elements.push({
+        kind: 'list',
+        rowCount: count,
+        name: property.name,
+        type: property.dataType,
+        value: (row: number) => {
+            const start = offsets[row]
+            const end = offsets[row + 1]
+            for (let i = start; i < end; ++i) {
+                listValue.entries[i - start] = column.value(i)
+            }
+            listValue.count = end - start
+            return listValue
+        }
+    })
+}
+
+async function parseInternal(data: string, ctx: RuntimeContext): Promise<Result<PlyFile>> {
+    const state = State(data, ctx);
+    ctx.update({ message: 'Parsing...', current: 0, max: data.length });
+    parseHeader(state)
+    // console.log(state.comments)
+    // console.log(JSON.stringify(state.elementSpecs, undefined, 4))
+    parseElements(state)
+    const { elements, elementSpecs, comments } = state
+    const elementNames = elementSpecs.map(s => s.name)
+    const result = PlyFile(elements, elementNames, comments)
+    return Result.success(result);
+}
+
+export function parse(data: string) {
+    return Task.create<Result<PlyFile>>('Parse PLY', async ctx => {
+        return await parseInternal(data, ctx)
+    })
+}
+
+export default parse;

+ 79 - 0
src/mol-io/reader/ply/schema.ts

@@ -0,0 +1,79 @@
+/**
+ * Copyright (c) 2019 mol* contributors, licensed under MIT, See LICENSE file for more info.
+ *
+ * @author Alexander Rose <alexander.rose@weirdbyte.de>
+ */
+
+import { Column } from 'mol-data/db';
+
+// http://paulbourke.net/dataformats/ply/
+// https://en.wikipedia.org/wiki/PLY_(file_format)
+
+export const PlyTypeByteLength = {
+    'char': 1,
+    'uchar': 1,
+    'short': 2,
+    'ushort': 2,
+    'int': 4,
+    'uint': 4,
+    'float': 4,
+    'double': 8,
+
+    'int8': 1,
+    'uint8': 1,
+    'int16': 2,
+    'uint16': 2,
+    'int32': 4,
+    'uint32': 4,
+    'float32': 4,
+    'float64': 8
+}
+export type PlyType = keyof typeof PlyTypeByteLength
+export const PlyTypes = new Set(Object.keys(PlyTypeByteLength))
+export function PlyType(str: string) {
+    if (!PlyTypes.has(str)) throw new Error(`unknown ply type '${str}'`)
+    return str as PlyType
+}
+
+export interface PlyFile {
+    readonly comments: ReadonlyArray<string>
+    readonly elementNames: ReadonlyArray<string>
+    getElement(name: string): PlyElement | undefined
+}
+
+export function PlyFile(elements: PlyElement[], elementNames: string[], comments: string[]): PlyFile {
+    const elementMap = new Map<string, PlyElement>()
+    for (let i = 0, il = elementNames.length; i < il; ++i) {
+        elementMap.set(elementNames[i], elements[i])
+    }
+    return {
+        comments,
+        elementNames,
+        getElement: (name: string) => {
+            return elementMap.get(name)
+        }
+    };
+}
+
+export type PlyElement = PlyTable | PlyList
+
+export interface PlyTable {
+    readonly kind: 'table'
+    readonly rowCount: number
+    readonly propertyNames: ReadonlyArray<string>
+    readonly propertyTypes: ReadonlyArray<PlyType>
+    getProperty(name: string): Column<number> | undefined
+}
+
+export interface PlyListValue {
+    readonly entries: ArrayLike<number>
+    readonly count: number
+}
+
+export interface PlyList {
+    readonly kind: 'list'
+    readonly rowCount: number,
+    readonly name: string,
+    readonly type: PlyType,
+    value: (row: number) => PlyListValue
+}

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

@@ -45,8 +45,8 @@ export const GaussianDensitySchema = {
 }
 
 export const GaussianDensityShaderCode = ShaderCode(
-    require('mol-gl/shader/gaussian-density.vert'),
-    require('mol-gl/shader/gaussian-density.frag'),
+    require('mol-gl/shader/gaussian-density.vert').default,
+    require('mol-gl/shader/gaussian-density.frag').default,
     { standardDerivatives: false, fragDepth: false }
 )
 

+ 23 - 0
src/mol-math/linear-algebra/3d/vec3.ts

@@ -414,6 +414,7 @@ namespace Vec3 {
     }
 
     const angleTempA = zero(), angleTempB = zero();
+    /** Computes the angle between 2 vectors, reports in rad. */
     export function angle(a: Vec3, b: Vec3) {
         copy(angleTempA, a);
         copy(angleTempB, b);
@@ -433,6 +434,28 @@ namespace Vec3 {
         }
     }
 
+    const tmp_dh_ab = Vec3.zero();
+    const tmp_dh_cb = Vec3.zero();
+    const tmp_dh_bc = Vec3.zero();
+    const tmp_dh_dc = Vec3.zero();
+    const tmp_dh_abc = Vec3.zero();
+    const tmp_dh_bcd = Vec3.zero();
+    const tmp_dh_cross = Vec3.zero();
+    /** Computes the dihedral angles of 4 related atoms. phi: C, N+1, CA+1, C+1 - psi: N, CA, C, N+1 - omega: CA, C, N+1, CA+1 */
+    export function dihedralAngle(a: Vec3, b: Vec3, c: Vec3, d: Vec3) {
+        Vec3.sub(tmp_dh_ab, a, b);
+        Vec3.sub(tmp_dh_cb, c, b);
+        Vec3.sub(tmp_dh_bc, b, c);
+        Vec3.sub(tmp_dh_dc, d, c);
+
+        Vec3.cross(tmp_dh_abc, tmp_dh_ab, tmp_dh_cb);
+        Vec3.cross(tmp_dh_bcd, tmp_dh_bc, tmp_dh_dc);
+
+        const angle = Vec3.angle(tmp_dh_abc, tmp_dh_bcd) * 360.0 / (2 * Math.PI);
+        Vec3.cross(tmp_dh_cross, tmp_dh_abc, tmp_dh_bcd);
+        return Vec3.dot(tmp_dh_cb, tmp_dh_cross) > 0 ? angle : -angle;
+    }
+
     /**
      * Returns whether or not the vectors have exactly the same elements in the same position (when compared with ===)
      */

+ 14 - 0
src/mol-math/linear-algebra/_spec/vec3.spec.ts

@@ -0,0 +1,14 @@
+import { Vec3 } from '../3d'
+
+describe('vec3', () => {
+    const vec1 = [ 1, 2, 3 ] as Vec3;
+    const vec2 = [ 2, 3, 1 ] as Vec3;
+    const orthVec1 = [ 0, 1, 0 ] as Vec3;
+    const orthVec2 = [ 1, 0, 0 ] as Vec3;
+
+    it('angle calculation', () => {
+        expect(Vec3.angle(vec1, vec1) * 360 / (2 * Math.PI)).toBe(0.0);
+        expect(Vec3.angle(orthVec1, orthVec2) * 360 / (2 * Math.PI)).toBe(90.0);
+        expect(Vec3.angle(vec1, vec2)).toBeCloseTo(0.666946);
+    });
+})

+ 260 - 0
src/mol-model-formats/shape/ply.ts

@@ -0,0 +1,260 @@
+/**
+ * Copyright (c) 2019 mol* contributors, licensed under MIT, See LICENSE file for more info.
+ *
+ * @author Schäfer, Marco <marco.schaefer@uni-tuebingen.de>
+ * @author Alexander Rose <alexander.rose@weirdbyte.de>
+ */
+
+import { RuntimeContext, Task } from 'mol-task';
+import { ShapeProvider } from 'mol-model/shape/provider';
+import { Color } from 'mol-util/color';
+import { PlyFile, PlyTable, PlyList } from 'mol-io/reader/ply/schema';
+import { MeshBuilder } from 'mol-geo/geometry/mesh/mesh-builder';
+import { Mesh } from 'mol-geo/geometry/mesh/mesh';
+import { Shape } from 'mol-model/shape';
+import { ChunkedArray } from 'mol-data/util';
+import { arrayMax, fillSerial } from 'mol-util/array';
+import { Column } from 'mol-data/db';
+import { ParamDefinition as PD } from 'mol-util/param-definition';
+import { ColorNames } from 'mol-util/color/tables';
+import { deepClone } from 'mol-util/object';
+
+// TODO support 'edge' element, see https://www.mathworks.com/help/vision/ug/the-ply-format.html
+// TODO support missing face element
+
+function createPlyShapeParams(plyFile?: PlyFile) {
+    const vertex = plyFile && plyFile.getElement('vertex') as PlyTable
+    const material = plyFile && plyFile.getElement('material') as PlyTable
+
+    const defaultValues = { group: '', vRed: '', vGreen: '', vBlue: '', mRed: '', mGreen: '', mBlue: '' }
+
+    const groupOptions: [string, string][] = [['', '']]
+    const colorOptions: [string, string][] = [['', '']]
+    if (vertex) {
+        for (let i = 0, il = vertex.propertyNames.length; i < il; ++i) {
+            const name = vertex.propertyNames[i]
+            const type = vertex.propertyTypes[i]
+            if (
+                type === 'uchar' || type === 'uint8' ||
+                type === 'ushort' || type === 'uint16' ||
+                type === 'uint' || type === 'uint32'
+            ) groupOptions.push([ name, name ])
+            if (type === 'uchar' || type === 'uint8') colorOptions.push([ name, name ])
+        }
+
+        // TODO hardcoded as convenience for data provided by MegaMol
+        if (vertex.propertyNames.includes('atomid')) defaultValues.group = 'atomid'
+        else if (vertex.propertyNames.includes('material_index')) defaultValues.group = 'material_index'
+
+        if (vertex.propertyNames.includes('red')) defaultValues.vRed = 'red'
+        if (vertex.propertyNames.includes('green')) defaultValues.vGreen = 'green'
+        if (vertex.propertyNames.includes('blue')) defaultValues.vBlue = 'blue'
+    }
+
+    const materialOptions: [string, string][] = [['', '']]
+    if (material) {
+        for (let i = 0, il = material.propertyNames.length; i < il; ++i) {
+            const name = material.propertyNames[i]
+            const type = material.propertyTypes[i]
+            if (type === 'uchar' || type === 'uint8') materialOptions.push([ name, name ])
+        }
+
+        if (material.propertyNames.includes('red')) defaultValues.mRed = 'red'
+        if (material.propertyNames.includes('green')) defaultValues.mGreen = 'green'
+        if (material.propertyNames.includes('blue')) defaultValues.mBlue = 'blue'
+    }
+
+    const defaultColoring = defaultValues.vRed && defaultValues.vGreen && defaultValues.vBlue ? 'vertex' :
+        defaultValues.mRed && defaultValues.mGreen && defaultValues.mBlue ? 'material' : 'uniform'
+
+    return {
+        ...Mesh.Params,
+
+        coloring: PD.MappedStatic(defaultColoring, {
+            vertex: PD.Group({
+                red: PD.Select(defaultValues.vRed, colorOptions, { label: 'Red Property' }),
+                green: PD.Select(defaultValues.vGreen, colorOptions, { label: 'Green Property' }),
+                blue: PD.Select(defaultValues.vBlue, colorOptions, { label: 'Blue Property' }),
+            }, { isFlat: true }),
+            material: PD.Group({
+                red: PD.Select(defaultValues.mRed, materialOptions, { label: 'Red Property' }),
+                green: PD.Select(defaultValues.mGreen, materialOptions, { label: 'Green Property' }),
+                blue: PD.Select(defaultValues.mBlue, materialOptions, { label: 'Blue Property' }),
+            }, { isFlat: true }),
+            uniform: PD.Group({
+                color: PD.Color(ColorNames.grey)
+            }, { isFlat: true })
+        }),
+        grouping: PD.MappedStatic(defaultValues.group ? 'vertex' : 'none', {
+            vertex: PD.Group({
+                group: PD.Select(defaultValues.group, groupOptions, { label: 'Group Property' }),
+            }, { isFlat: true }),
+            none: PD.Group({ })
+        }),
+    }
+}
+
+export const PlyShapeParams = createPlyShapeParams()
+export type PlyShapeParams = typeof PlyShapeParams
+
+async function getMesh(ctx: RuntimeContext, vertex: PlyTable, face: PlyList, groupIds: ArrayLike<number>, mesh?: Mesh) {
+    const builderState = MeshBuilder.createState(vertex.rowCount, vertex.rowCount / 4, mesh)
+    const { vertices, normals, indices, groups } = builderState
+
+    const x = vertex.getProperty('x')
+    const y = vertex.getProperty('y')
+    const z = vertex.getProperty('z')
+    if (!x || !y || !z) throw new Error('missing coordinate properties')
+
+    const nx = vertex.getProperty('nx')
+    const ny = vertex.getProperty('ny')
+    const nz = vertex.getProperty('nz')
+
+    const hasNormals = !!nx && !!ny && !!nz
+
+    for (let i = 0, il = vertex.rowCount; i < il; ++i) {
+        if (i % 100000 === 0 && ctx.shouldUpdate) await ctx.update({ current: i, max: il, message: `adding vertex ${i}` })
+
+        ChunkedArray.add3(vertices, x.value(i), y.value(i), z.value(i))
+        if (hasNormals) ChunkedArray.add3(normals, nx!.value(i), ny!.value(i), nz!.value(i));
+        ChunkedArray.add(groups, groupIds[i])
+    }
+
+    for (let i = 0, il = face.rowCount; i < il; ++i) {
+        if (i % 100000 === 0 && ctx.shouldUpdate) await ctx.update({ current: i, max: il, message: `adding face ${i}` })
+
+        const { entries, count } = face.value(i)
+        if (count === 3) {
+            // triangle
+            ChunkedArray.add3(indices, entries[0], entries[1], entries[2])
+        } else if (count === 4) {
+            // quadrilateral
+            ChunkedArray.add3(indices, entries[2], entries[1], entries[0])
+            ChunkedArray.add3(indices, entries[2], entries[0], entries[3])
+        }
+    }
+
+    const m = MeshBuilder.getMesh(builderState);
+    m.normalsComputed = hasNormals
+    await Mesh.computeNormals(m).runInContext(ctx)
+
+    return m
+}
+
+const int = Column.Schema.int
+
+type Grouping = { ids: ArrayLike<number>, map: ArrayLike<number> }
+function getGrouping(vertex: PlyTable, props: PD.Values<PlyShapeParams>): Grouping {
+    const { grouping } = props
+    const { rowCount } = vertex
+    const column = grouping.name === 'vertex' ? vertex.getProperty(grouping.params.group) : undefined
+
+    const ids = column ? column.toArray({ array: Uint32Array }) : fillSerial(new Uint32Array(rowCount))
+    const maxId = arrayMax(ids) // assumes uint ids
+    const map = new Uint32Array(maxId + 1)
+    for (let i = 0, il = ids.length; i < il; ++i) map[ids[i]] = i
+    return { ids, map }
+}
+
+type Coloring = { kind: 'vertex' | 'material' | 'uniform', red: Column<number>, green: Column<number>, blue: Column<number> }
+function getColoring(vertex: PlyTable, material: PlyTable | undefined, props: PD.Values<PlyShapeParams>): Coloring {
+    const { coloring } = props
+    const { rowCount } = vertex
+
+    let red: Column<number>, green: Column<number>, blue: Column<number>
+    if (coloring.name === 'vertex') {
+        red = vertex.getProperty(coloring.params.red) || Column.ofConst(127, rowCount, int)
+        green = vertex.getProperty(coloring.params.green) || Column.ofConst(127, rowCount, int)
+        blue = vertex.getProperty(coloring.params.blue) || Column.ofConst(127, rowCount, int)
+    } else if (coloring.name === 'material') {
+        red = (material && material.getProperty(coloring.params.red)) || Column.ofConst(127, rowCount, int)
+        green = (material && material.getProperty(coloring.params.green)) || Column.ofConst(127, rowCount, int)
+        blue = (material && material.getProperty(coloring.params.blue)) || Column.ofConst(127, rowCount, int)
+    } else {
+        const [r, g, b] = Color.toRgb(coloring.params.color)
+        red = Column.ofConst(r, rowCount, int)
+        green = Column.ofConst(g, rowCount, int)
+        blue = Column.ofConst(b, rowCount, int)
+    }
+    return { kind: coloring.name, red, green, blue }
+}
+
+function createShape(plyFile: PlyFile, mesh: Mesh, coloring: Coloring, grouping: Grouping) {
+    const { kind, red, green, blue } = coloring
+    const { ids, map } = grouping
+    return Shape.create(
+        'ply-mesh', plyFile, mesh,
+        (groupId: number) => {
+            const idx = kind === 'material' ? groupId : map[groupId]
+            return Color.fromRgb(red.value(idx), green.value(idx), blue.value(idx))
+        },
+        () => 1, // size: constant
+        (groupId: number) => {
+            return ids[groupId].toString()
+        }
+    )
+}
+
+function makeShapeGetter() {
+    let _plyFile: PlyFile | undefined
+    let _props: PD.Values<PlyShapeParams> | undefined
+
+    let _shape: Shape<Mesh>
+    let _mesh: Mesh
+    let _coloring: Coloring
+    let _grouping: Grouping
+
+    const getShape = async (ctx: RuntimeContext, plyFile: PlyFile, props: PD.Values<PlyShapeParams>, shape?: Shape<Mesh>) => {
+
+        const vertex = plyFile.getElement('vertex') as PlyTable
+        if (!vertex) throw new Error('missing vertex element')
+
+        const face = plyFile.getElement('face') as PlyList
+        if (!face) throw new Error('missing face element')
+
+        const material = plyFile.getElement('material') as PlyTable
+
+        let newMesh = false
+        let newColor = false
+
+        if (!_plyFile || _plyFile !== plyFile) {
+            newMesh = true
+        }
+
+        if (!_props || !PD.isParamEqual(PlyShapeParams.grouping, _props.grouping, props.grouping)) {
+            newMesh = true
+        }
+
+        if (!_props || !PD.isParamEqual(PlyShapeParams.coloring, _props.coloring, props.coloring)) {
+            newColor = true
+        }
+
+        if (newMesh) {
+            _coloring = getColoring(vertex, material, props)
+            _grouping = getGrouping(vertex, props)
+            _mesh = await getMesh(ctx, vertex, face, _grouping.ids, shape && shape.geometry)
+            _shape = createShape(plyFile, _mesh, _coloring, _grouping)
+        } else if (newColor) {
+            _coloring = getColoring(vertex, material, props)
+            _shape = createShape(plyFile, _mesh, _coloring, _grouping)
+        }
+
+        _plyFile = plyFile
+        _props = deepClone(props)
+
+        return _shape
+    }
+    return getShape
+}
+
+export function shapeFromPly(source: PlyFile, params?: {}) {
+    return Task.create<ShapeProvider<PlyFile, Mesh, PlyShapeParams>>('Shape Provider', async ctx => {
+        return {
+            label: 'Mesh',
+            data: source,
+            params: createPlyShapeParams(source),
+            getShape: makeShapeGetter(),
+            geometryUtils: Mesh.Utils
+        }
+    })
+}

+ 26 - 0
src/mol-model-formats/structure/_spec/pdb.spec.ts

@@ -0,0 +1,26 @@
+/**
+ * Copyright (c) 2019 mol* contributors, licensed under MIT, See LICENSE file for more info.
+ *
+ * @author Alexander Rose <alexander.rose@weirdbyte.de>
+ */
+
+import { guessElementSymbol } from '../pdb/to-cif';
+import { TokenBuilder } from 'mol-io/reader/common/text/tokenizer';
+
+const records = [
+    ['ATOM     19 HD23 LEU A   1     151.940 143.340 155.670  0.00  0.00', 'H'],
+    ['ATOM     38  CA  SER A   3     146.430 138.150 162.270  0.00  0.00', 'C'],
+    ['ATOM     38 NA   SER A   3     146.430 138.150 162.270  0.00  0.00', 'NA'],
+    ['ATOM     38  NAA SER A   3     146.430 138.150 162.270  0.00  0.00', 'N'],
+]
+
+describe('PDB to-cif', () => {
+    it('guess-element-symbol', () => {
+        for (let i = 0, il = records.length; i < il; ++i) {
+            const [ data, element ] = records[i]
+            const tokens = TokenBuilder.create(data, 2)
+            guessElementSymbol(tokens, data, 12, 16)
+            expect(data.substring(tokens.indices[0], tokens.indices[1])).toBe(element)
+        }
+    });
+});

+ 40 - 4
src/mol-model-formats/structure/pdb/to-cif.ts

@@ -8,7 +8,7 @@
 import { substringStartsWith } from 'mol-util/string';
 import { CifField, CifCategory, CifFrame } from 'mol-io/reader/cif';
 import { mmCIF_Schema } from 'mol-io/reader/cif/schema/mmcif';
-import { TokenBuilder, Tokenizer } from 'mol-io/reader/common/text/tokenizer';
+import { TokenBuilder, Tokenizer, Tokens } from 'mol-io/reader/common/text/tokenizer';
 import { PdbFile } from 'mol-io/reader/pdb/schema';
 import { parseCryst1, parseRemark350, parseMtrix } from './assembly';
 import { WaterNames } from 'mol-model/structure/model/types';
@@ -89,6 +89,43 @@ function getEntityId(residueName: string, isHet: boolean) {
     return '1';
 }
 
+export function guessElementSymbol(tokens: Tokens, str: string, start: number, end: number) {
+    let s = start, e = end - 1
+
+    // trim spaces and numbers
+    let c = str.charCodeAt(s)
+    while ((c === 32 || (c >= 48 && c <= 57)) && s <= e) c = str.charCodeAt(++s)
+    c = str.charCodeAt(e)
+    while ((c === 32 || (c >= 48 && c <= 57)) && e >= s) c = str.charCodeAt(--e)
+
+    ++e
+
+    if (s === e) return TokenBuilder.add(tokens, s, e) // empty
+    if (s + 1 === e) return TokenBuilder.add(tokens, s, e) // one char
+
+    c = str.charCodeAt(s)
+
+    if (s + 2 === e) { // two chars
+        const c2 = str.charCodeAt(s + 1)
+        if (
+            ((c === 78 || c === 110) && (c2 === 65 || c2 ===  97)) || // NA na Na nA
+            ((c === 67 || c ===  99) && (c2 === 76 || c2 === 108)) || // CL
+            ((c === 70 || c === 102) && (c2 === 69 || c2 === 101))    // FE
+        ) return TokenBuilder.add(tokens, s, s + 2)
+    }
+
+    if (
+        c === 67 || c ===  99 || // C c
+        c === 72 || c === 104 || // H h
+        c === 78 || c === 110 || // N n
+        c === 79 || c === 111 || // O o
+        c === 80 || c === 112 || // P p
+        c === 83 || c === 115    // S s
+    ) return TokenBuilder.add(tokens, s, s + 1)
+
+    TokenBuilder.add(tokens, s, s) // no reasonable guess, add empty token
+}
+
 function addAtom(sites: AtomSiteTemplate, model: string, data: Tokenizer, s: number, e: number, isHet: boolean) {
     const { data: str } = data;
     const length = e - s;
@@ -162,11 +199,10 @@ function addAtom(sites: AtomSiteTemplate, model: string, data: Tokenizer, s: num
         if (data.tokenStart < data.tokenEnd) {
             TokenBuilder.addToken(sites.type_symbol, data);
         } else {
-            // "guess" the symbol
-            TokenBuilder.add(sites.type_symbol, s + 12, s + 13);
+            guessElementSymbol(sites.type_symbol, str, s + 12, s + 16)
         }
     } else {
-        TokenBuilder.add(sites.type_symbol, s + 12, s + 13);
+        guessElementSymbol(sites.type_symbol, str, s + 12, s + 16)
     }
 
     sites.label_entity_id[sites.index] = getEntityId(residueName, isHet);

+ 16 - 0
src/mol-model/shape/provider.ts

@@ -0,0 +1,16 @@
+/**
+ * Copyright (c) 2019 mol* contributors, licensed under MIT, See LICENSE file for more info.
+ *
+ * @author Alexander Rose <alexander.rose@weirdbyte.de>
+ */
+
+import { ShapeGetter } from 'mol-repr/shape/representation';
+import { Geometry, GeometryUtils } from 'mol-geo/geometry/geometry';
+
+export interface ShapeProvider<D, G extends Geometry, P extends Geometry.Params<G>> {
+    label: string
+    data: D
+    params: P
+    getShape: ShapeGetter<D, G, P>
+    geometryUtils: GeometryUtils<G>
+}

+ 4 - 1
src/mol-model/shape/shape.ts

@@ -15,6 +15,8 @@ export interface Shape<G extends Geometry = Geometry> {
     readonly id: UUID
     /** A name to describe the shape */
     readonly name: string
+    /** The data used to create the shape */
+    readonly sourceData: unknown
     /** The geometry of the shape, e.g. `Mesh` or `Lines` */
     readonly geometry: G
     /** An array of transformation matrices to describe multiple instances of the geometry */
@@ -30,10 +32,11 @@ export interface Shape<G extends Geometry = Geometry> {
 }
 
 export namespace Shape {
-    export function create<G extends Geometry>(name: string, geometry: G, getColor: Shape['getColor'], getSize: Shape['getSize'], getLabel: Shape['getLabel'], transforms?: Mat4[]): Shape<G> {
+    export function create<G extends Geometry>(name: string, sourceData: unknown, geometry: G, getColor: Shape['getColor'], getSize: Shape['getSize'], getLabel: Shape['getLabel'], transforms?: Mat4[]): Shape<G> {
         return {
             id: UUID.create22(),
             name,
+            sourceData,
             geometry,
             transforms: transforms || [Mat4.identity()],
             get groupCount() { return Geometry.getGroupCount(geometry) },

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

@@ -17,12 +17,17 @@ interface SecondaryStructure {
 }
 
 namespace SecondaryStructure {
-    export type Element = None | Helix | Sheet
+    export type Element = None | Turn | Helix | Sheet
 
     export interface None {
         kind: 'none'
     }
 
+    export interface Turn {
+        kind: 'turn',
+        flags: SecondaryStructureType
+    }
+
     export interface Helix {
         kind: 'helix',
         flags: SecondaryStructureType,

+ 1 - 1
src/mol-model/structure/model/properties/utils/guess-element.ts

@@ -12,7 +12,7 @@ function charAtIsNumber(str: string, index: number) {
     return code >= 48 && code <= 57
 }
 
-export function guessElement (str: string) {
+export function guessElement(str: string) {
     let at = str.trim().toUpperCase()
 
     if (charAtIsNumber(at, 0)) at = at.substr(1)

+ 494 - 105
src/mol-model/structure/model/properties/utils/secondary-structure.ts

@@ -2,6 +2,7 @@
  * Copyright (c) 2019 mol* contributors, licensed under MIT, See LICENSE file for more info.
  *
  * @author Alexander Rose <alexander.rose@weirdbyte.de>
+ * @author Sebastian Bittrich <sebastian.bittrich@rcsb.org>
  */
 
 import { SecondaryStructure } from 'mol-model/structure/model/properties/seconday-structure';
@@ -14,13 +15,120 @@ import { IntAdjacencyGraph } from 'mol-math/graph';
 import { BitFlags } from 'mol-util';
 import { ElementIndex } from 'mol-model/structure/model/indexing';
 import { AtomicHierarchy, AtomicConformation } from '../atomic';
+import { ParamDefinition as PD } from 'mol-util/param-definition'
 
-export function computeSecondaryStructure(hierarchy: AtomicHierarchy, conformation: AtomicConformation): SecondaryStructure {
+/**
+ * TODO bugs to fix:
+ * - some turns are not detected correctly: see e.g. pdb:1acj - maybe more than 2 hbonds require some residue to donate electrons
+ * - some sheets are not extended correctly: see e.g. pdb:1acj
+ * - validate new helix definition
+ * - validate new ordering of secondary structure elements
+ */
+
+ /** max distance between two C-alpha atoms to check for hbond */
+const caMaxDist = 9.0;
+
+/**
+ * Constant for electrostatic energy in kcal/mol
+ *      f  *  q1 *   q2
+ * Q = -332 * 0.42 * 0.20
+ *
+ * f is the dimensional factor
+ *
+ * q1 and q2 are partial charges which are placed on the C,O
+ * (+q1,-q1) and N,H (-q2,+q2)
+ */
+const Q = -27.888
+
+/** cutoff for hbonds in kcal/mol, must be lower to be consider as an hbond */
+const hbondEnergyCutoff = -0.5
+/** prevent extremely low hbond energies */
+const hbondEnergyMinimal = -9.9
+
+interface DSSPContext {
+    params: Partial<PD.Values<SecondaryStructureComputationParams>>,
+    getResidueFlag: (f: DSSPType) => SecondaryStructureType,
+    getFlagName: (f: DSSPType) => String,
+
+    hierarchy: AtomicHierarchy
+    proteinResidues: SortedArray<ResidueIndex>
+    /** flags for each residue */
+    flags: Uint32Array
+    hbonds: DsspHbonds,
+
+    torsionAngles: { phi: Float32Array, psi: Float32Array },
+    backboneIndices: BackboneAtomIndices,
+    conformation: AtomicConformation,
+    ladders: Ladder[],
+    bridges: Bridge[]
+}
+
+interface Ladder {
+    previousLadder: number,
+    nextLadder: number,
+    firstStart: number,
+    secondStart: number,
+    secondEnd: number,
+    firstEnd: number,
+    type: BridgeType
+}
+
+const enum BridgeType {
+    PARALLEL = 0x0,
+    ANTI_PARALLEL = 0x1
+}
+
+class Bridge {
+    partner1: number;
+    partner2: number;
+    type: BridgeType;
+
+    constructor(p1: number, p2: number, type: BridgeType) {
+        this.partner1 = Math.min(p1, p2)
+        this.partner2 = Math.max(p1, p2)
+        this.type = type
+    }
+}
+
+type DSSPType = BitFlags<DSSPType.Flag>
+namespace DSSPType {
+    export const is: (t: DSSPType, f: Flag) => boolean = BitFlags.has
+    export const create: (f: Flag) => DSSPType = BitFlags.create
+    export const enum Flag {
+        _ = 0x0,
+        H = 0x1,
+        B = 0x2,
+        E = 0x4,
+        G = 0x8,
+        I = 0x10,
+        S = 0x20,
+        T = 0x40,
+        T3 = 0x80,
+        T4 = 0x100,
+        T5 = 0x200,
+        T3S = 0x400, // marks 3-turn start
+        T4S = 0x800,
+        T5S = 0x1000
+    }
+}
+
+export const SecondaryStructureComputationParams = {
+    oldDefinition: PD.Boolean(true, { description: 'Whether to use the old DSSP convention for the annotation of turns and helices, causes them to be two residues shorter' }),
+    oldOrdering: PD.Boolean(true, { description: 'Alpha-helices are preferred over 3-10 helices' })
+}
+export type SecondaryStructureComputationParams = typeof SecondaryStructureComputationParams
+
+export function computeSecondaryStructure(hierarchy: AtomicHierarchy,
+    conformation: AtomicConformation) {
     // TODO use Zhang-Skolnik for CA alpha only parts or for coarse parts with per-residue elements
     return computeModelDSSP(hierarchy, conformation)
 }
 
-export function computeModelDSSP(hierarchy: AtomicHierarchy, conformation: AtomicConformation) {
+export function computeModelDSSP(hierarchy: AtomicHierarchy,
+    conformation: AtomicConformation,
+    params: Partial<PD.Values<SecondaryStructureComputationParams>> = {}): SecondaryStructure {
+    params = { ...PD.getDefaultValues(SecondaryStructureComputationParams), ...params };
+
     const { lookup3d, proteinResidues } = calcAtomicTraceLookup3D(hierarchy, conformation)
     const backboneIndices = calcBackboneAtomIndices(hierarchy, proteinResidues)
     const hbonds = calcBackboneHbonds(hierarchy, conformation, proteinResidues, backboneIndices, lookup3d)
@@ -28,69 +136,103 @@ export function computeModelDSSP(hierarchy: AtomicHierarchy, conformation: Atomi
     const residueCount = proteinResidues.length
     const flags = new Uint32Array(residueCount)
 
+    // console.log(`calculating secondary structure elements using ${ params.oldDefinition ? 'old' : 'revised'} definition and ${ params.oldOrdering ? 'old' : 'revised'} ordering of secondary structure elements`)
+
+    const torsionAngles = calculateDihedralAngles(hierarchy, conformation, proteinResidues, backboneIndices)
+
+    const ladders: Ladder[] = []
+    const bridges: Bridge[] = []
+
+    const getResidueFlag = params.oldDefinition ? getOriginalResidueFlag : getUpdatedResidueFlag
+    const getFlagName = params.oldOrdering ? getOriginalFlagName : getUpdatedFlagName
+
     const ctx: DSSPContext = {
+        params,
+        getResidueFlag,
+        getFlagName,
+
         hierarchy,
         proteinResidues,
         flags,
-        hbonds
+        hbonds,
+
+        torsionAngles,
+        backboneIndices,
+        conformation,
+        ladders,
+        bridges
     }
 
-    assignBends(ctx)
     assignTurns(ctx)
     assignHelices(ctx)
+    assignBends(ctx)
     assignBridges(ctx)
     assignLadders(ctx)
     assignSheets(ctx)
 
-    const assignment = getDSSPAssignment(flags)
-
+    const assignment = getDSSPAssignment(flags, getResidueFlag)
     const type = new Uint32Array(hierarchy.residues._rowCount) as unknown as SecondaryStructureType[]
+    const keys: number[] = []
+    const elements: SecondaryStructure.Element[] = []
+
     for (let i = 0, il = proteinResidues.length; i < il; ++i) {
-        type[proteinResidues[i]] = assignment[i]
+        const assign = assignment[i]
+        type[proteinResidues[i]] = assign
+        const flag = getResidueFlag(flags[i])
+        // TODO is this expected behavior? elements will be strictly split depending on 'winning' flag
+        if (elements.length === 0 /* would fail at very start */ || flag !== (elements[elements.length - 1] as SecondaryStructure.Helix | SecondaryStructure.Sheet | SecondaryStructure.Turn).flags /* flag changed */) {
+            elements[elements.length] = createElement(mapToKind(assign), flags[i], getResidueFlag)
+        }
+        keys[i] = elements.length - 1
     }
 
     const secondaryStructure: SecondaryStructure = {
         type,
-        key: [], // TODO
-        elements: [] // TODO
+        key: keys,
+        elements: elements
     }
+
     return secondaryStructure
 }
 
-interface DSSPContext {
-    hierarchy: AtomicHierarchy
-    proteinResidues: SortedArray<ResidueIndex>
-    /** flags for each residue */
-    flags: Uint32Array
-
-    hbonds: DsspHbonds
+function createElement(kind: string, flag: DSSPType.Flag, getResidueFlag: (f: DSSPType) => SecondaryStructureType): SecondaryStructure.Element {
+    // TODO would be nice to add more detailed information
+    if (kind === 'helix') {
+        return {
+            kind: 'helix',
+            flags: getResidueFlag(flag)
+        } as SecondaryStructure.Helix
+    } else if (kind === 'sheet') {
+        return {
+            kind: 'sheet',
+            flags: getResidueFlag(flag)
+        } as SecondaryStructure.Sheet
+    } else if (kind === 'turn' || kind === 'bend') {
+        return {
+            kind: 'turn',
+            flags: getResidueFlag(flag)
+        }
+    } else {
+        return {
+            kind: 'none'
+        }
+    }
 }
 
-type DSSPType = BitFlags<DSSPType.Flag>
-namespace DSSPType {
-    export const is: (t: DSSPType, f: Flag) => boolean = BitFlags.has
-    export const create: (f: Flag) => DSSPType = BitFlags.create
-    export const enum Flag {
-        _ = 0x0,
-        H = 0x1,
-        B = 0x2,
-        E = 0x4,
-        G = 0x8,
-        I = 0x10,
-        S = 0x20,
-        T = 0x40,
-        T3 = 0x80,
-        T4 = 0x100,
-        T5 = 0x200,
+function mapToKind(assignment: SecondaryStructureType.Flag) {
+    if (assignment === SecondaryStructureType.SecondaryStructureDssp.H || assignment === SecondaryStructureType.SecondaryStructureDssp.G || assignment === SecondaryStructureType.SecondaryStructureDssp.I) {
+        return 'helix'
+    } else if (assignment === SecondaryStructureType.SecondaryStructureDssp.B || assignment === SecondaryStructureType.SecondaryStructureDssp.E) {
+        return 'sheet'
+    } else if (assignment === SecondaryStructureType.SecondaryStructureDssp.T) {
+        return 'turn'
+    } else if (assignment === SecondaryStructureType.SecondaryStructureDssp.S) {
+        return 'bend'
+    } else {
+        return 'none'
     }
 }
 
-/** max distance between two C-alpha atoms to check for hbond */
-const caMaxDist = 7.0;
-
-/** min distance between two C-alpha atoms to check for hbond */
-const caMinDist = 4.0;
-
 function calcAtomicTraceLookup3D(hierarchy: AtomicHierarchy, conformation: AtomicConformation) {
     const { x, y, z } = conformation;
     const { moleculeType, traceElementIndex } = hierarchy.derived.residue
@@ -141,6 +283,104 @@ function calcBackboneAtomIndices(hierarchy: AtomicHierarchy, proteinResidues: So
 
 type DsspHbonds = IntAdjacencyGraph<{ readonly energies: ArrayLike<number> }>
 
+/**
+ * Bend(i) =: [angle ((CW - Ca(i - 2)),(C"(i + 2) - C"(i))) > 70"]
+ *
+ * Type: S
+ */
+function assignBends(ctx: DSSPContext) {
+    const flags = ctx.flags
+    const { x, y, z } = ctx.conformation
+    const { traceElementIndex } = ctx.hierarchy.derived.residue
+
+    const proteinResidues = ctx.proteinResidues
+    const residueCount = proteinResidues.length
+
+    const position = (i: number, v: Vec3) => Vec3.set(v, x[i], y[i], z[i])
+
+    const caPosPrev2 = Vec3.zero()
+    const caPos = Vec3.zero()
+    const caPosNext2 = Vec3.zero()
+
+    const nIndices = ctx.backboneIndices.nIndices
+    const cPos = Vec3.zero()
+    const nPosNext = Vec3.zero()
+
+    f1: for (let i = 2; i < residueCount - 2; i++) {
+        // check for peptide bond
+        for (let k = 0; k < 4; k++) {
+            let index = i + k - 2
+            position(traceElementIndex[index], cPos)
+            position(nIndices[index + 1], nPosNext)
+            if (Vec3.squaredDistance(cPos, nPosNext) > 6.25 /* max squared peptide bond distance allowed */) {
+                continue f1
+            }
+        }
+
+        const oRIprev2 = proteinResidues[i - 2]
+        const oRI = proteinResidues[i]
+        const oRInext2 = proteinResidues[i + 2]
+
+        const caAtomPrev2 = traceElementIndex[oRIprev2]
+        const caAtom = traceElementIndex[oRI]
+        const caAtomNext2 = traceElementIndex[oRInext2]
+
+        position(caAtomPrev2, caPosPrev2)
+        position(caAtom, caPos)
+        position(caAtomNext2, caPosNext2)
+
+        const caMinus2 = Vec3.zero()
+        const caPlus2 = Vec3.zero()
+
+        Vec3.sub(caMinus2, caPosPrev2, caPos)
+        Vec3.sub(caPlus2, caPos, caPosNext2)
+
+        const angle = Vec3.angle(caMinus2, caPlus2) * 360 / (2 * Math.PI)
+        if (angle && angle > 70.00) {
+            flags[i] |= DSSPType.Flag.S
+        }
+    }
+}
+
+function calculateDihedralAngles(hierarchy: AtomicHierarchy, conformation: AtomicConformation, proteinResidues: SortedArray<ResidueIndex>, backboneIndices: BackboneAtomIndices): { phi: Float32Array, psi: Float32Array } {
+    const { cIndices, nIndices } = backboneIndices
+    const { index } = hierarchy
+    const { x, y, z } = conformation
+    const { traceElementIndex } = hierarchy.derived.residue
+
+    const residueCount = proteinResidues.length
+    const position = (i: number, v: Vec3) => Vec3.set(v, x[i], y[i], z[i])
+
+    let cPosPrev = Vec3.zero(), caPosPrev = Vec3.zero(), nPosPrev = Vec3.zero()
+    let cPos = Vec3.zero(), caPos = Vec3.zero(), nPos = Vec3.zero()
+    let cPosNext = Vec3.zero(), caPosNext = Vec3.zero(), nPosNext = Vec3.zero()
+
+    const phi: Float32Array = new Float32Array(residueCount - 1)
+    const psi: Float32Array = new Float32Array(residueCount - 1)
+
+    const cAtomPrev = cIndices[-1], caAtomPrev = traceElementIndex[proteinResidues[-1]], nAtomPrev = nIndices[-1]
+    position(cAtomPrev, cPosPrev), position(caAtomPrev, caPosPrev), position(nAtomPrev, nPosPrev)
+    const cAtom = cIndices[0], caAtom = traceElementIndex[proteinResidues[0]], nAtom = nIndices[0]
+    position(cAtom, cPos), position(caAtom, caPos), position(nAtom, nPos)
+    const cAtomNext = cIndices[1], caAtomNext = traceElementIndex[proteinResidues[1]], nAtomNext = nIndices[1]
+    position(cAtomNext, cPosNext), position(caAtomNext, caPosNext), position(nAtomNext, nPosNext)
+    for (let i = 0; i < residueCount - 1; ++i) {
+        // ignore C-terminal residue as acceptor
+        if (index.findAtomOnResidue(proteinResidues[i], 'OXT') !== -1) continue
+
+        // returns NaN for missing atoms
+        phi[i] = Vec3.dihedralAngle(cPosPrev, nPos, caPos, cPos)
+        psi[i] = Vec3.dihedralAngle(nPos, caPos, cPos, nPosNext)
+
+        cPosPrev = cPos, caPosPrev = caPos, nPosPrev = nPos
+        cPos = cPosNext, caPos = caPosNext, nPos = nPosNext
+
+        position(cIndices[i + 1], cPosNext), position(traceElementIndex[proteinResidues[i + 1]], caPosNext), position(nIndices[i + 1], nPosNext)
+    }
+
+    return { phi, psi };
+}
+
 function calcBackboneHbonds(hierarchy: AtomicHierarchy, conformation: AtomicConformation, proteinResidues: SortedArray<ResidueIndex>, backboneIndices: BackboneAtomIndices, lookup3d: GridLookup3D): DsspHbonds {
     const { cIndices, hIndices, nIndices, oIndices } = backboneIndices
     const { index } = hierarchy
@@ -163,8 +403,6 @@ function calcBackboneHbonds(hierarchy: AtomicHierarchy, conformation: AtomicConf
     const cPosPrev = Vec3.zero()
     const oPosPrev = Vec3.zero()
 
-    const caMinDistSq = caMinDist * caMinDist
-
     for (let i = 0, il = proteinResidues.length; i < il; ++i) {
         const oPI = i
         const oRI = proteinResidues[i]
@@ -183,11 +421,9 @@ function calcBackboneHbonds(hierarchy: AtomicHierarchy, conformation: AtomicConf
         position(cAtom, cPos)
         position(caAtom, caPos)
 
-        const { indices, count, squaredDistances } = lookup3d.find(caPos[0], caPos[1], caPos[2], caMaxDist)
+        const { indices, count } = lookup3d.find(caPos[0], caPos[1], caPos[2], caMaxDist)
 
         for (let j = 0; j < count; ++j) {
-            if (squaredDistances[j] < caMinDistSq) continue
-
             const nPI = indices[j]
 
             // ignore bonds within a residue or to prev or next residue, TODO take chain border into account
@@ -245,8 +481,8 @@ function buildHbondGraph(residueCount: number, oAtomResidues: number[], nAtomRes
 /** Original priority: H,B,E,G,I,T,S */
 function getOriginalResidueFlag(f: DSSPType) {
     if (DSSPType.is(f, DSSPType.Flag.H)) return SecondaryStructureType.SecondaryStructureDssp.H
-    if (DSSPType.is(f, DSSPType.Flag.B)) return SecondaryStructureType.SecondaryStructureDssp.B
     if (DSSPType.is(f, DSSPType.Flag.E)) return SecondaryStructureType.SecondaryStructureDssp.E
+    if (DSSPType.is(f, DSSPType.Flag.B)) return SecondaryStructureType.SecondaryStructureDssp.B
     if (DSSPType.is(f, DSSPType.Flag.G)) return SecondaryStructureType.SecondaryStructureDssp.G
     if (DSSPType.is(f, DSSPType.Flag.I)) return SecondaryStructureType.SecondaryStructureDssp.I
     if (DSSPType.is(f, DSSPType.Flag.T)) return SecondaryStructureType.SecondaryStructureDssp.T
@@ -254,55 +490,50 @@ function getOriginalResidueFlag(f: DSSPType) {
     return SecondaryStructureType.Flag.None
 }
 
+function getOriginalFlagName(f: DSSPType) {
+    if (DSSPType.is(f, DSSPType.Flag.H)) return 'H'
+    if (DSSPType.is(f, DSSPType.Flag.E)) return 'E'
+    if (DSSPType.is(f, DSSPType.Flag.B)) return 'B'
+    if (DSSPType.is(f, DSSPType.Flag.G)) return 'G'
+    if (DSSPType.is(f, DSSPType.Flag.I)) return 'I'
+    if (DSSPType.is(f, DSSPType.Flag.T)) return 'T'
+    if (DSSPType.is(f, DSSPType.Flag.S)) return 'S'
+    return '-'
+}
+
 /** Version 2.1.0 priority: I,H,B,E,G,T,S */
 function getUpdatedResidueFlag(f: DSSPType) {
     if (DSSPType.is(f, DSSPType.Flag.I)) return SecondaryStructureType.SecondaryStructureDssp.I
     if (DSSPType.is(f, DSSPType.Flag.H)) return SecondaryStructureType.SecondaryStructureDssp.H
-    if (DSSPType.is(f, DSSPType.Flag.B)) return SecondaryStructureType.SecondaryStructureDssp.B
     if (DSSPType.is(f, DSSPType.Flag.E)) return SecondaryStructureType.SecondaryStructureDssp.E
+    if (DSSPType.is(f, DSSPType.Flag.B)) return SecondaryStructureType.SecondaryStructureDssp.B
     if (DSSPType.is(f, DSSPType.Flag.G)) return SecondaryStructureType.SecondaryStructureDssp.G
     if (DSSPType.is(f, DSSPType.Flag.T)) return SecondaryStructureType.SecondaryStructureDssp.T
     if (DSSPType.is(f, DSSPType.Flag.S)) return SecondaryStructureType.SecondaryStructureDssp.S
     return SecondaryStructureType.Flag.None
 }
 
-// function geFlagName(f: DSSPType) {
-//     if (DSSPType.is(f, DSSPType.Flag.I)) return 'I'
-//     if (DSSPType.is(f, DSSPType.Flag.H)) return 'H'
-//     if (DSSPType.is(f, DSSPType.Flag.B)) return 'B'
-//     if (DSSPType.is(f, DSSPType.Flag.E)) return 'E'
-//     if (DSSPType.is(f, DSSPType.Flag.G)) return 'G'
-//     if (DSSPType.is(f, DSSPType.Flag.T)) return 'T'
-//     if (DSSPType.is(f, DSSPType.Flag.S)) return 'S'
-//     return '-'
-// }
-
-function getDSSPAssignment(flags: Uint32Array, useOriginal = false) {
-    const getResidueFlag = useOriginal ? getOriginalResidueFlag : getUpdatedResidueFlag
+function getUpdatedFlagName(f: DSSPType) {
+    if (DSSPType.is(f, DSSPType.Flag.I)) return 'I'
+    if (DSSPType.is(f, DSSPType.Flag.H)) return 'H'
+    if (DSSPType.is(f, DSSPType.Flag.E)) return 'E'
+    if (DSSPType.is(f, DSSPType.Flag.B)) return 'B'
+    if (DSSPType.is(f, DSSPType.Flag.G)) return 'G'
+    if (DSSPType.is(f, DSSPType.Flag.T)) return 'T'
+    if (DSSPType.is(f, DSSPType.Flag.S)) return 'S'
+    return '-'
+}
+
+function getDSSPAssignment(flags: Uint32Array, getResidueFlag: (f: DSSPType) => SecondaryStructureType) {
     const type = new Uint32Array(flags.length)
     for (let i = 0, il = flags.length; i < il; ++i) {
         const f = DSSPType.create(flags[i])
-        // console.log(i, geFlagName(f))
         type[i] = getResidueFlag(f)
     }
+
     return type as unknown as ArrayLike<SecondaryStructureType>
 }
 
-/**
- * Constant for electrostatic energy in kcal/mol
- *      f  *  q1 *   q2
- * Q = -332 * 0.42 * 0.20
- *
- * f is the dimensional factor
- *
- * q1 and q2 are partial charges which are placed on the C,O
- * (+q1,-q1) and N,H (-q2,+q2)
- */
-const Q = -27.888
-
-/** cutoff for hbonds in kcal/mol, must be lower to be consider as an hbond */
-const hbondEnergyCutoff = -0.5
-
 /**
  * E = Q * (1/r(ON) + l/r(CH) - l/r(OH) - l/r(CN))
  */
@@ -314,7 +545,13 @@ function calcHbondEnergy(oPos: Vec3, cPos: Vec3, nPos: Vec3, hPos: Vec3) {
 
     const e1 = Q / distOH - Q / distCH
     const e2 = Q / distCN - Q / distON
-    return e1 + e2
+    const e = e1 + e2
+
+    // cap lowest possible energy
+    if (e < hbondEnergyMinimal)
+        return hbondEnergyMinimal
+
+    return e
 }
 
 /**
@@ -329,24 +566,31 @@ function assignTurns(ctx: DSSPContext) {
     const { chains, residueAtomSegments, chainAtomSegments } = hierarchy
     const { label_asym_id } = chains
 
-    const turnFlag = [0, 0, 0, DSSPType.Flag.T3, DSSPType.Flag.T4, DSSPType.Flag.T5]
+    const turnFlag = [DSSPType.Flag.T3S, DSSPType.Flag.T4S, DSSPType.Flag.T5S, DSSPType.Flag.T3, DSSPType.Flag.T4, DSSPType.Flag.T5]
 
-    for (let i = 0, il = proteinResidues.length; i < il; ++i) {
-        const rI = proteinResidues[i]
-        const cI = chainAtomSegments.index[residueAtomSegments.offsets[rI]]
+    for (let idx = 0; idx < 3; idx++) {
+        for (let i = 0, il = proteinResidues.length - 1; i < il; ++i) {
+            const rI = proteinResidues[i]
+            const cI = chainAtomSegments.index[residueAtomSegments.offsets[rI]]
 
-        // TODO should take sequence gaps into account
-        for (let k = 3; k <= 5; ++k) {
-            if (i + k >= proteinResidues.length) continue
-
-            const rN = proteinResidues[i + k]
+            // TODO should take sequence gaps into account
+            const rN = proteinResidues[i + idx + 3]
             const cN = chainAtomSegments.index[residueAtomSegments.offsets[rN]]
             // check if on same chain
             if (!label_asym_id.areValuesEqual(cI, cN)) continue
 
             // check if hbond exists
-            if (hbonds.getDirectedEdgeIndex(i, i + k) !== -1) {
-                flags[i] |= turnFlag[k] | DSSPType.Flag.T
+            if (hbonds.getDirectedEdgeIndex(i, i + idx + 3) !== -1) {
+                flags[i] |= turnFlag[idx + 3] | turnFlag[idx]
+                if (ctx.params.oldDefinition) {
+                    for (let k = 1; k < idx + 3; ++k) {
+                        flags[i + k] |= turnFlag[idx + 3] | DSSPType.Flag.T
+                    }
+                } else {
+                    for (let k = 0; k <= idx + 3; ++k) {
+                        flags[i + k] |= turnFlag[idx + 3] | DSSPType.Flag.T
+                    }
+                }
             }
         }
     }
@@ -369,7 +613,7 @@ function assignTurns(ctx: DSSPContext) {
  * Type: B
  */
 function assignBridges(ctx: DSSPContext) {
-    const { proteinResidues, hbonds, flags } = ctx
+    const { proteinResidues, hbonds, flags, bridges } = ctx
 
     const { offset, b } = hbonds
     let i: number, j: number
@@ -385,6 +629,8 @@ function assignBridges(ctx: DSSPContext) {
             if (i !== j && hbonds.getDirectedEdgeIndex(j, i + 1) !== -1) {
                 flags[i] |= DSSPType.Flag.B
                 flags[j] |= DSSPType.Flag.B
+                // TODO move to constructor, actually omit object all together
+                bridges[bridges.length] = new Bridge(i, j, BridgeType.PARALLEL)
             }
 
             // Parallel Bridge(i, j) =: [Hbond(j - 1, i) and Hbond(i, j + 1)]
@@ -393,6 +639,7 @@ function assignBridges(ctx: DSSPContext) {
             if (i !== j && hbonds.getDirectedEdgeIndex(j - 1, i) !== -1) {
                 flags[i] |= DSSPType.Flag.B
                 flags[j] |= DSSPType.Flag.B
+                bridges[bridges.length] = new Bridge(j, i, BridgeType.PARALLEL)
             }
 
             // Antiparallel Bridge(i, j) =: [Hbond(i, j) and Hbond(j, i)]
@@ -401,6 +648,7 @@ function assignBridges(ctx: DSSPContext) {
             if (i !== j && hbonds.getDirectedEdgeIndex(j, i) !== -1) {
                 flags[i] |= DSSPType.Flag.B
                 flags[j] |= DSSPType.Flag.B
+                bridges[bridges.length] = new Bridge(j, i, BridgeType.ANTI_PARALLEL)
             }
 
             // Antiparallel Bridge(i, j) =: [Hbond(i - 1, j + 1) and Hbond(j - 1, i + l)]
@@ -409,9 +657,12 @@ function assignBridges(ctx: DSSPContext) {
             if (i !== j && hbonds.getDirectedEdgeIndex(j - 1, i + 1) !== -1) {
                 flags[i] |= DSSPType.Flag.B
                 flags[j] |= DSSPType.Flag.B
+                bridges[bridges.length] = new Bridge(j, i, BridgeType.ANTI_PARALLEL)
             }
         }
     }
+
+    bridges.sort((a, b) => a.partner1 > b.partner1 ? 1 : a.partner1 < b.partner1 ? -1 : 0)
 }
 
 /**
@@ -428,17 +679,41 @@ function assignBridges(ctx: DSSPContext) {
 function assignHelices(ctx: DSSPContext) {
     const { proteinResidues, flags } = ctx
 
-    const turnFlag = [0, 0, 0, DSSPType.Flag.T3, DSSPType.Flag.T4, DSSPType.Flag.T5]
+    const turnFlag = [DSSPType.Flag.T3S, DSSPType.Flag.T4S, DSSPType.Flag.T5S, DSSPType.Flag.T3, DSSPType.Flag.T4, DSSPType.Flag.T5]
     const helixFlag = [0, 0, 0, DSSPType.Flag.G, DSSPType.Flag.H, DSSPType.Flag.I]
 
-    for (let i = 1, il = proteinResidues.length; i < il; ++i) {
-        const fI = DSSPType.create(flags[i])
-        const fI1 = DSSPType.create(flags[i - 1])
+    const helixCheckOrder = ctx.params.oldOrdering ? [4, 3, 5] : [3, 4, 5]
+    for (let ni = 0; ni < helixCheckOrder.length; ni++) {
+        const n = helixCheckOrder[ni]
+
+        for (let i = 1, il = proteinResidues.length - n; i < il; i++) {
+            const fI = DSSPType.create(flags[i])
+            const fI1 = DSSPType.create(flags[i - 1])
+            const fI2 = DSSPType.create(flags[i + 1])
 
-        for (let k = 3; k <= 5; ++k) {
-            if (DSSPType.is(fI, turnFlag[k]) && DSSPType.is(fI1, turnFlag[k])) {
-                for (let l = 0; l < k; ++l) {
-                    flags[i + l] |= helixFlag[k]
+            // TODO rework to elegant solution which will not break instantly
+            if (ctx.params.oldOrdering) {
+                if ((n === 3 && (DSSPType.is(fI, DSSPType.Flag.H) || DSSPType.is(fI2, DSSPType.Flag.H)) || // for 3-10 yield to alpha helix
+                    (n === 5 && ((DSSPType.is(fI, DSSPType.Flag.H) || DSSPType.is(fI, DSSPType.Flag.G)) || (DSSPType.is(fI2, DSSPType.Flag.H) || DSSPType.is(fI2, DSSPType.Flag.G)))))) { // for pi yield to all other helices
+                    continue
+                }
+            } else {
+                if ((n === 4 && (DSSPType.is(fI, DSSPType.Flag.G) || DSSPType.is(fI2, DSSPType.Flag.G)) || // for alpha helix yield to 3-10
+                    (n === 5 && ((DSSPType.is(fI, DSSPType.Flag.H) || DSSPType.is(fI, DSSPType.Flag.G)) || (DSSPType.is(fI2, DSSPType.Flag.H) || DSSPType.is(fI2, DSSPType.Flag.G)))))) { // for pi yield to all other helices
+                    continue
+                }
+            }
+
+            if (DSSPType.is(fI, turnFlag[n]) && DSSPType.is(fI, turnFlag[n - 3]) && // check fI for turn start of proper type
+                DSSPType.is(fI1, turnFlag[n]) && DSSPType.is(fI1, turnFlag[n - 3])) { // check fI1 accordingly
+                if (ctx.params.oldDefinition) {
+                    for (let k = 0; k < n; k++) {
+                        flags[i + k] |= helixFlag[n]
+                    }
+                } else {
+                    for (let k = -1; k <= n; k++) {
+                        flags[i + k] |= helixFlag[n]
+                    }
                 }
             }
         }
@@ -451,23 +726,137 @@ function assignHelices(ctx: DSSPContext) {
  * Type: E
  */
 function assignLadders(ctx: DSSPContext) {
-    // TODO
+    const { bridges, ladders } = ctx
+
+    // create ladders
+    for (let bridgeIndex = 0; bridgeIndex < bridges.length; bridgeIndex++) {
+        const bridge = bridges[bridgeIndex]
+        let found = false
+        for (let ladderIndex = 0; ladderIndex < ladders.length; ladderIndex++) {
+            const ladder = ladders[ladderIndex]
+            if (shouldExtendLadder(ladder, bridge)) {
+                found = true
+                ladder.firstEnd++
+                if (bridge.type === BridgeType.PARALLEL) {
+                    ladder.secondEnd++
+                } else {
+                    ladder.secondStart--
+                }
+            }
+        }
+
+        // no suitable assignment: create new ladder with single bridge as content
+        if (!found) {
+            ladders[ladders.length] = {
+                previousLadder: 0,
+                nextLadder: 0,
+                firstStart: bridge.partner1,
+                firstEnd: bridge.partner1,
+                secondStart: bridge.partner2,
+                secondEnd: bridge.partner2,
+                type: bridge.type
+            }
+        }
+    }
+
+    // connect ladders
+    for (let ladderIndex1 = 0; ladderIndex1 < ladders.length; ladderIndex1++) {
+        const ladder1 = ladders[ladderIndex1]
+        for (let ladderIndex2 = ladderIndex1; ladderIndex2 < ladders.length; ladderIndex2++) {
+            const ladder2 = ladders[ladderIndex2]
+            if (resemblesBulge(ladder1, ladder2)) {
+                ladder1.nextLadder = ladderIndex2
+                ladder2.previousLadder = ladderIndex1
+            }
+        }
+    }
 }
 
 /**
- * sheet=: set of one or more ladders connected by shared residues
- *
- * Type: E
+ * For beta structures, we define: a bulge-linked ladder consists of two ladders or bridges of the same type
+ * connected by at most one extra residue of one strand and at most four extra residues  on the other strand,
+ * all residues in bulge-linked ladders are marked E, including any extra residues.
  */
-function assignSheets(ctx: DSSPContext) {
-    // TODO
+function resemblesBulge(ladder1: Ladder, ladder2: Ladder) {
+    if (!(ladder1.type === ladder2.type && ladder2.firstStart - ladder1.firstEnd < 6 &&
+        ladder1.firstStart < ladder2.firstStart && ladder2.nextLadder === 0)) return false
+
+    if (ladder1.type === BridgeType.PARALLEL) {
+        return bulgeCriterion2(ladder1, ladder2)
+    } else {
+        return bulgeCriterion2(ladder2, ladder1)
+    }
+}
+
+function bulgeCriterion2(ladder1: Ladder, ladder2: Ladder) {
+    return ladder2.secondStart - ladder1.secondEnd > 0 && ((ladder2.secondStart - ladder1.secondEnd < 6 &&
+        ladder2.firstStart - ladder1.firstEnd < 3) || ladder2.secondStart - ladder1.secondEnd < 3)
+}
+
+function shouldExtendLadder(ladder: Ladder, bridge: Bridge): boolean {
+    // in order to extend ladders, same type must be present
+    if (bridge.type !== ladder.type) return false
+
+    // only extend if residue 1 is sequence neighbor with regard to ladder
+    if (bridge.partner1 !== ladder.firstEnd + 1) return false
+
+    if (bridge.type === BridgeType.PARALLEL) {
+        if (bridge.partner2 === ladder.secondEnd + 1) {
+            return true
+        }
+    } else {
+        if (bridge.partner2 === ladder.secondStart - 1) {
+            return true
+        }
+    }
+
+    return false
+}
+
+function isHelixType(f: DSSPType) {
+    return DSSPType.is(f, DSSPType.Flag.G) || DSSPType.is(f, DSSPType.Flag.H) || DSSPType.is(f, DSSPType.Flag.I)
 }
 
 /**
- * Bend(i) =: [angle ((CW - Ca(i - 2)),(C"(i + 2) - C"(i))) > 70"]
+ * sheet=: set of one or more ladders connected by shared residues
  *
- * Type: S
+ * Type: E
  */
-function assignBends(ctx: DSSPContext) {
-    // TODO
+function assignSheets(ctx: DSSPContext) {
+    const { ladders, flags } = ctx
+    for (let ladderIndex = 0; ladderIndex < ladders.length; ladderIndex++) {
+        const ladder = ladders[ladderIndex]
+        for (let lcount = ladder.firstStart; lcount <= ladder.firstEnd; lcount++) {
+            const diff = ladder.firstStart - lcount
+            const l2count = ladder.secondStart - diff
+
+            if (ladder.firstStart !== ladder.firstEnd) {
+                flags[lcount] |= DSSPType.Flag.E
+                flags[l2count] |= DSSPType.Flag.E
+            } else {
+                if (!isHelixType(flags[lcount]) && DSSPType.is(flags[lcount], DSSPType.Flag.E)) {
+                    flags[lcount] |= DSSPType.Flag.B
+                }
+                if (!isHelixType(flags[l2count]) && DSSPType.is(flags[l2count], DSSPType.Flag.E)) {
+                    flags[l2count] |= DSSPType.Flag.B
+                }
+            }
+        }
+
+        if (ladder.nextLadder === 0) continue
+
+        const conladder = ladders[ladder.nextLadder]
+        for (let lcount = ladder.firstStart; lcount <= conladder.firstEnd; lcount++) {
+            flags[lcount] |= DSSPType.Flag.E
+        }
+        if (ladder.type === BridgeType.PARALLEL) {
+            for (let lcount = ladder.secondStart; lcount <= conladder.secondEnd; lcount++) {
+                flags[lcount] |= DSSPType.Flag.E
+            }
+        } else {
+            for (let lcount = conladder.secondEnd; lcount <= ladder.secondStart; lcount++) {
+                flags[lcount] |= DSSPType.Flag.E
+            }
+        }
+    }
 }

+ 75 - 0
src/mol-model/structure/model/properties/utils/secondary-structure.validation

@@ -0,0 +1,75 @@
+compares Mol* port of DSSP (with default parameters) to the BioJava implementation
+
+### pdb:1pga ###
+# turns #
+Mol*: ----------------------TTTTTTTTTTTTTTTT--------TTTT------
+53 turns, 18 openings
+DSSP: ----------------------TTTTTTTTTTTTTTTT--------TTTT------
+53 turns, 18 openings
+
+# bends #
+Mol*: ---------SS---------SSSSSSSSSSSSSSSSSS--------SSS-------
+23 bends
+DSSP: ---------SS---------SSSSSSSSSSSSSSSSSS--------SSS-------
+23 bends
+
+# helices #
+Mol*: ----------------------HHHHHHHHHHHHHHTT--------TTTT------
+44 helix elements - 0 3-10, 44 alpha, 0 pi
+DSSP: ----------------------HHHHHHHHHHHHHHTT--------TTTT------
+44 helix elements - 0 3-10, 44 alpha, 0 pi
+
+# all #
+Mol*: -EEEEEEE-SS-EEEEEEE-SSHHHHHHHHHHHHHHTT---EEEEETTTTEEEEE-
+DSSP: -EEEEEEE-SS-EEEEEEE-SSHHHHHHHHHHHHHHTT---EEEEETTTTEEEEE-
+
+
+### pdb:1bta ###
+# turns #
+Mol*: ------TTT---TTTTTTTTTTTTT--TT----TTTTTTTTTTT-----------TTTTTTTTT--TTTTTTTTTTTTTTT--------
+127 turns, 44 openings
+DSSP: ------TTT---TTTTTTTTTTTTT--TT----TTTTTTTTTTT-----------TTTTTTTTT--TTTTTTTTTTTTTTT--------
+127 turns, 44 openings
+
+# bends #
+Mol*: ------SSS--SSSSSSSSSSSSS---SS---SSSSSSSSSSSSS-SS------SSSSSSSSSSSSSSSSSSSSSSSSSSS--------
+60 bends
+DSSP: ------SSS--SSSSSSSSSSSSS---SS---SSSSSSSSSSSSS-SS------SSSSSSSSSSSSSSSSSSSSSSSSSSS--------
+60 bends
+
+# helices #
+Mol*: ------TTT---HHHHHHHHHHHHT--TT----HHHHHHHHTTT-----------TTHHHHTTT--HHHHHHHHHHHHHTT--------
+100 helix elements - 0 3-10, 100 alpha, 0 pi
+DSSP: ------TTT---HHHHHHHHHHHHT--TT----HHHHHHHHTTT-----------TTHHHHTTT--HHHHHHHHHHHHHTT--------
+100 helix elements - 0 3-10, 100 alpha, 0 pi
+
+# all #
+Mol*: -EEEEETTT--SHHHHHHHHHHHHT--TT---SHHHHHHHHTTTS-SSEEEEEESTTHHHHTTTSSHHHHHHHHHHHHHTT--EEEEE-
+DSSP: -EEEEETTT--SHHHHHHHHHHHHT--TT---SHHHHHHHHTTTS-SSEEEEEESTTHHHHTTTSSHHHHHHHHHHHHHTT--EEEEE-
+
+
+### pdb:1acj ###
+# turns #
+Mol*: -------TT----------TT----------------TTTTT----------------------------TTTT-TTTTTT----------------------------------TTT------TTT-TTTTTTTTT-----------TTTT---TT-------TTTTTTTTTTTTTTTTTTTTT--TT-------TTTTTTTTTTTT-TTTTTT----------TT-TT----TTTTTTTTTTTTTTTT-----TTTTTTTTTT--TTTTTTTTTTT-----------------------TTTTTTTT----------------TTTTTTT-TT--TT------TTTTTTTTTTTTTT--TTTTTTTTTTT--TTTTT-TTTTTTTTTTTTTTTTTTTTTTTTTTTTTTT-------------TT----TTT---TTTTTTTTTTTTT-TTT---TTTTTTTTTTTTTTTTTTTT--------------------------------TTTTTTTTTTTTTTTTTTT-
+614 turns, 223 openings
+DSSP: -------TT----------TT----------------TTTTT------------------------------TT-TTTTTT----------------------------------TTT------TTT-TTTTTTTTT-----------TTTT---TT-------TTTTTTTTTTTTTTTTTTTTT--TT-------TTTTTTTTTTTT-TTTTTT----------TT-TT----TTTTTTTTTTTTTTTT-----TTTTTTTTTT--TTTTTTTTTTT-----------------------TTTTTTTT----------------TTTTTTT-TT--TT------TTTTTTTTTTT-TT--TTTTTTTTTTT--TTTTT-TTTTTTTTTTTTTTTTTTTTTTTTTTTTTTT-------------TT----TTT---TTTTTTTTTTTTT-TTT---TTTTTTTTTTTTTTTTTTTT--------------------------------TTTTTTTTTTTTTTTTTTT-
+606 turns, 220 openings
+
+# bends #
+Mol*: --S----SS----------SSS----------S----SS-SSS--------SS-----S-----------SSSS-SSSSSSS--S---S----------SS--SS---------SSSS---S-SSSS--SSSSSSS-----------SSSS----SS-SSS-S-SSSSSSSSSSSSSSSSSSSSS--SSS------SSSSSSSSSSSS-SSSSSS-S----SS--SSSSSS---SSSSSSSSSSSSSSS----S-SSSSSSSSSSS-SSSSSSSSSS--SS--SS--S------SSSSSS-SSSSSSS--S--S-------S--SSSSSSSSSSS--SSS-----SSSSSSSSSSS-SS--SSSSSSSSSSS--SSSSS-SSSSSSSSSSSSSSSSS----SSSSSSSSSSSS-----------SS--S-SSS-S-SS-SSSSSS--SS-SSS---SSSSSSSSSSSSSSSSSSSSSSSS---------SSS------SSSS----SSSS-S----SSSSSSSSSS--
+305 bends
+DSSP: --S----SS----------SSS----------S----SS-SSS--------SS-----S-----------SSSS-SSSSSSS--S---S----------SS--SS---------SSSS---S-SSSS--SSSSSSS-----------SSSS----SS-SSS-S-SSSSSSSSSSSSSSSSSSSSS--SSS------SSSSSSSSSSSS-SSSSSS-S----SS--SSSSSS---SSSSSSSSSSSSSSS----S-SSSSSSSSSSS-SSSSSSSSSS--SS--SS--S------SSSSSS-SSSSSSS--S--S-------S--SSSSSSSSSSS--SSS-----SSSSSSSSSSS-SS--SSSSSSSSSSS--SSSSS-SSSSSSSSSSSSSSSSS----SSSSSSSSSSSS-----------SS--S-SSS-S-SS-SSSSSS--SS-SSS---SSSSSSSSSSSSSSSSSSSSSSSS---------SSS------SSSS----SSSS-S----SSSSSSSSSS--
+305 bends
+
+# helices #
+Mol*: -------TT----------TT----------------GGGTT----------------------------TTTT-HHHHTT----------------------------------TTT------GGG-THHHHHHHT-----------HHHH---TT-------HHHHHHHHHHHHHHHHGGGGT--TT-------THHHHHHHHHHH-HHHHTT----------TT-TT----HHHHHHHHHHHHHHTT-----HHHHHHHHHH--HHHHHHHHGGG-----------------------HHHHHHHT----------------HHHHHHH-TT--TT------HHHHHHHHHHHTTT--HHHHHHHHHHH--GGGTT-HHHHHHHHHHHHHHHHTHHHHHHHHHHHHTT-------------TT----GGG---TTTTHHHHTTGGG-GGG---HHHHHHHHHHHHHHHHHHHT--------------------------------TTHHHHHHHHTHHHHHHHH-
+523 helix elements - 27 3-10, 496 alpha, 0 pi
+DSSP: -------TT----------TT----------------GGGTT------------------------------TT-HHHHTT----------------------------------TTT------GGG-THHHHHHHT-----------HHHH---TT-------HHHHHHHHHHHHHHHHGGGGT--TT-------THHHHHHHHHHH-HHHHTT----------TT-TT----HHHHHHHHHHHHHHTT-----HHHHHHHHHH--HHHHHHHHGGG-----------------------HHHHHHHT----------------HHHHHHH-TT--TT------HHHHHHHHHHH-TT--HHHHHHHHHHH--GGGTT-HHHHHHHHHHHHHHHHTHHHHHHHHHHHHTT-------------TT----GGG---TTTTHHHHTTGGG-GGG---HHHHHHHHHHHHHHHHHHHT--------------------------------TTHHHHHHHHTHHHHHHHH-
+523 helix elements - 27 3-10, 496 alpha, 0 pi
+
+# all #
+Mol*: --SEEEETTEEEE-EEEEETTEEEEEEEEEE-EE---GGGTTS--EE----SSEEE--S---B-------TTTT-HHHHTTS--S-B-S---EEEEEE-SS--SSEEEEEEE--STTT---S-SGGG-THHHHHHHT-EEEE-----SHHHH---TT-SSS-S-HHHHHHHHHHHHHHHHGGGGTEEEEEEEEEEETHHHHHHHHHHH-HHHHTT-SEEEEES--TTSTTSEEEHHHHHHHHHHHHHHTT---S-HHHHHHHHHHS-HHHHHHHHGGG-SS--SS--S--EEE-SSSSSS-HHHHHHHT-S--S-EEEEEESB-SHHHHHHHSTT--TTS-----HHHHHHHHHHHTTT--HHHHHHHHHHH--GGGTT-HHHHHHHHHHHHHHHHTHHHHHHHHHHHHTTSS-EEEEEE----TT--S-GGG-SBTTTTHHHHTTGGG-GGG---HHHHHHHHHHHHHHHHHHHTSSSS---------SSS-EEEEESSSS--EEESTTHHHHHHHHTHHHHHHHH-
+DSSP: --SEEEETTEEEE-EEEEETTEEEEEEEEEE-EE---GGGTTS--EE----SSEEE--S---B-------SSTT-HHHHTTS--S-B-S---EEEEEE-SS--SSEEEEEEE--STTT---S-SGGG-THHHHHHHT-EEEE-----SHHHH---TT-SSS-S-HHHHHHHHHHHHHHHHGGGGTEEEEEEEEEEETHHHHHHHHHHH-HHHHTT-SEEEEES--TTSTTS-EEHHHHHHHHHHHHHHTT---S-HHHHHHHHHHS-HHHHHHHHGGG-SS--SS--S---EE-SSSSSS-HHHHHHHT-S--S-EEEEEESB-SHHHHHHHSTT--TTS-----HHHHHHHHHHH-TT--HHHHHHHHHHH--GGGTT-HHHHHHHHHHHHHHHHTHHHHHHHHHHHHTTSS-EEEEEE----TT--S-GGG-SBTTTTHHHHTTGGG-GGG---HHHHHHHHHHHHHHHHHHHTSSSS---------SSS-EEEEESSSS--EEESTTHHHHHHHHTHHHHHHHH-
+
+TODO fix mismatches                                                   e.g. here
+TODO move to spec.ts once tests are running

+ 32 - 31
src/mol-model/structure/model/types.ts

@@ -275,41 +275,42 @@ export namespace SecondaryStructureType {
         DoubleHelix = 0x1,
         Helix = 0x2,
         Beta = 0x4,
-        Turn = 0x8,
+        Bend = 0x8,
+        Turn = 0x10,
 
         // category variant
-        LeftHanded = 0x10,  // helix
-        RightHanded = 0x20,
+        LeftHanded = 0x20,  // helix
+        RightHanded = 0x40,
 
-        ClassicTurn = 0x40,  // turn
-        InverseTurn = 0x80,
+        ClassicTurn = 0x80,  // turn
+        InverseTurn = 0x100,
 
         // sub-category
-        HelixOther = 0x100,  // protein
-        Helix27 = 0x200,
-        Helix3Ten = 0x400,
-        HelixAlpha = 0x800,
-        HelixGamma = 0x1000,
-        HelixOmega = 0x2000,
-        HelixPi = 0x4000,
-        HelixPolyproline = 0x8000,
-
-        DoubleHelixOther = 0x10000,  // nucleic
-        DoubleHelixZ = 0x20000,
-        DoubleHelixA = 0x40000,
-        DoubleHelixB = 0x80000,
-
-        BetaOther = 0x100000,  // protein
-        BetaStrand = 0x200000,  // single strand
-        BetaSheet = 0x400000,  // multiple hydrogen bonded strands
-        BetaBarell = 0x800000,  // closed series of sheets
-
-        TurnOther = 0x1000000,  // protein
-        Turn1 = 0x2000000,
-        Turn2 = 0x4000000,
-        Turn3 = 0x8000000,
-
-        NA = 0x10000000,  // not applicable/available
+        HelixOther = 0x200,  // protein
+        Helix27 = 0x400,
+        Helix3Ten = 0x800,
+        HelixAlpha = 0x1000,
+        HelixGamma = 0x2000,
+        HelixOmega = 0x4000,
+        HelixPi = 0x8000,
+        HelixPolyproline = 0x10000,
+
+        DoubleHelixOther = 0x20000,  // nucleic
+        DoubleHelixZ = 0x40000,
+        DoubleHelixA = 0x80000,
+        DoubleHelixB = 0x100000,
+
+        BetaOther = 0x200000,  // protein
+        BetaStrand = 0x400000,  // single strand
+        BetaSheet = 0x800000,  // multiple hydrogen bonded strands
+        BetaBarell = 0x1000000,  // closed series of sheets
+
+        TurnOther = 0x2000000,  // protein
+        Turn1 = 0x4000000,
+        Turn2 = 0x8000000,
+        Turn3 = 0x10000000,
+
+        NA = 0x20000000,  // not applicable/available
     }
 
     export const SecondaryStructureMmcif: { [value: string]: number } = {
@@ -386,7 +387,7 @@ export namespace SecondaryStructureType {
         G: Flag.Helix | Flag.Helix3Ten,  // 3-helix (310 helix)
         I: Flag.Helix | Flag.HelixPi,  // 5 helix (pi-helix)
         T: Flag.Turn,  // hydrogen bonded turn
-        S: Flag.Turn,  // bend
+        S: Flag.Bend,  // bend
     }
 }
 

+ 1 - 1
src/mol-plugin/behavior/dynamic/labels.ts

@@ -107,7 +107,7 @@ export const SceneLabels = PluginBehavior.create<SceneLabelsProps>({
 
         private getLabelsShape = (ctx: RuntimeContext, data: LabelsData, props: SceneLabelsProps, shape?: Shape<Text>) => {
             this.geo = getLabelsText(data, props, this.geo)
-            return Shape.create('Scene Labels', this.geo, this.getColor, this.getSize, this.getLabel, data.transforms)
+            return Shape.create('Scene Labels', data, this.geo, this.getColor, this.getSize, this.getLabel, data.transforms)
         }
 
         /** Update structures to be labeled, returns true if changed */

+ 2 - 0
src/mol-plugin/state/actions/data-format.ts

@@ -14,6 +14,7 @@ import { Ccp4Provider, Dsn6Provider, DscifProvider } from './volume';
 import { StateTransforms } from '../transforms';
 import { MmcifProvider, PdbProvider, GroProvider } from './structure';
 import msgpackDecode from 'mol-io/common/msgpack/decode'
+import { PlyProvider } from './shape';
 
 export class DataFormatRegistry<D extends PluginStateObject.Data.Binary | PluginStateObject.Data.String> {
     private _list: { name: string, provider: DataFormatProvider<D> }[] = []
@@ -60,6 +61,7 @@ export class DataFormatRegistry<D extends PluginStateObject.Data.Binary | Plugin
         this.add('gro', GroProvider)
         this.add('mmcif', MmcifProvider)
         this.add('pdb', PdbProvider)
+        this.add('ply', PlyProvider)
     };
 
     private _clear() {

+ 31 - 0
src/mol-plugin/state/actions/shape.ts

@@ -0,0 +1,31 @@
+/**
+ * Copyright (c) 2019 mol* contributors, licensed under MIT, See LICENSE file for more info.
+ *
+ * @author Alexander Rose <alexander.rose@weirdbyte.de>
+ */
+
+import { PluginContext } from 'mol-plugin/context';
+import { State, StateBuilder } from 'mol-state';
+import { Task } from 'mol-task';
+import { FileInfo } from 'mol-util/file-info';
+import { PluginStateObject } from '../objects';
+import { StateTransforms } from '../transforms';
+import { DataFormatProvider } from './data-format';
+
+export const PlyProvider: DataFormatProvider<any> = {
+    label: 'PLY',
+    description: 'PLY',
+    stringExtensions: ['ply'],
+    binaryExtensions: [],
+    isApplicable: (info: FileInfo, data: string) => {
+        return info.ext === 'ply'
+    },
+    getDefaultBuilder: (ctx: PluginContext, data: StateBuilder.To<PluginStateObject.Data.String>, state: State) => {
+        return Task.create('PLY default builder', async taskCtx => {
+            const tree = data.apply(StateTransforms.Data.ParsePly)
+                .apply(StateTransforms.Model.ShapeFromPly)
+                .apply(StateTransforms.Representation.ShapeRepresentation3D)
+            await state.updateTree(tree).runInContext(taskCtx)
+        })
+    }
+}

+ 10 - 0
src/mol-plugin/state/objects.ts

@@ -6,6 +6,7 @@
  */
 
 import { CifFile } from 'mol-io/reader/cif';
+import { PlyFile } from 'mol-io/reader/ply/schema';
 import { Model as _Model, Structure as _Structure } from 'mol-model/structure';
 import { VolumeData } from 'mol-model/volume';
 import { PluginBehavior } from 'mol-plugin/behavior/behavior';
@@ -16,6 +17,8 @@ import { StateObject, StateTransformer } from 'mol-state';
 import { Ccp4File } from 'mol-io/reader/ccp4/schema';
 import { Dsn6File } from 'mol-io/reader/dsn6/schema';
 import { ShapeRepresentation } from 'mol-repr/shape/representation';
+import { Shape as _Shape } from 'mol-model/shape';
+import { ShapeProvider } from 'mol-model/shape/provider';
 
 export type TypeClass = 'root' | 'data' | 'prop'
 
@@ -61,6 +64,7 @@ export namespace PluginStateObject {
     export namespace Format {
         export class Json extends Create<any>({ name: 'JSON Data', typeClass: 'Data' }) { }
         export class Cif extends Create<CifFile>({ name: 'CIF File', typeClass: 'Data' }) { }
+        export class Ply extends Create<PlyFile>({ name: 'PLY File', typeClass: 'Data' }) { }
         export class Ccp4 extends Create<Ccp4File>({ name: 'CCP4/MRC/MAP File', typeClass: 'Data' }) { }
         export class Dsn6 extends Create<Dsn6File>({ name: 'DSN6/BRIX File', typeClass: 'Data' }) { }
 
@@ -71,6 +75,7 @@ export namespace PluginStateObject {
             | { kind: 'cif', data: CifFile }
             | { kind: 'ccp4', data: Ccp4File }
             | { kind: 'dsn6', data: Dsn6File }
+            | { kind: 'ply', data: PlyFile }
             // For non-build in extensions
             | { kind: 'custom', data: unknown, tag: string })
         export type BlobData = BlobEntry[]
@@ -100,6 +105,11 @@ export namespace PluginStateObject {
         export class Data extends Create<VolumeData>({ name: 'Volume Data', typeClass: 'Object' }) { }
         export class Representation3D extends CreateRepresentation3D<VolumeRepresentation<any>>({ name: 'Volume 3D' }) { }
     }
+
+    export namespace Shape {
+        export class Provider extends Create<ShapeProvider<any, any, any>>({ name: 'Shape Provider', typeClass: 'Object' }) { }
+        export class Representation3D extends CreateRepresentation3D<ShapeRepresentation<any, any, any>>({ name: 'Shape 3D' }) { }
+    }
 }
 
 export namespace PluginStateTransform {

+ 18 - 0
src/mol-plugin/state/transforms/data.ts

@@ -15,6 +15,7 @@ import { StateTransformer } from 'mol-state';
 import { readFromFile, ajaxGetMany } from 'mol-util/data-source';
 import * as CCP4 from 'mol-io/reader/ccp4/parser'
 import * as DSN6 from 'mol-io/reader/dsn6/parser'
+import * as PLY from 'mol-io/reader/ply/parser'
 
 export { Download }
 type Download = typeof Download
@@ -185,6 +186,23 @@ const ParseCif = PluginStateTransform.BuiltIn({
     }
 });
 
+export { ParsePly }
+type ParsePly = typeof ParsePly
+const ParsePly = PluginStateTransform.BuiltIn({
+    name: 'parse-ply',
+    display: { name: 'Parse PLY', description: 'Parse PLY from String data' },
+    from: [SO.Data.String],
+    to: SO.Format.Ply
+})({
+    apply({ a }) {
+        return Task.create('Parse PLY', async ctx => {
+            const parsed = await PLY.parse(a.data).runInContext(ctx);
+            if (parsed.isError) throw new Error(parsed.message);
+            return new SO.Format.Ply(parsed.result, { label: parsed.result.comments[0] || 'PLY Data' });
+        });
+    }
+});
+
 export { ParseCcp4 }
 type ParseCcp4 = typeof ParseCcp4
 const ParseCcp4 = PluginStateTransform.BuiltIn({

+ 22 - 2
src/mol-plugin/state/transforms/model.ts

@@ -24,6 +24,7 @@ import { trajectoryFromGRO } from 'mol-model-formats/structure/gro';
 import { parseGRO } from 'mol-io/reader/gro/parser';
 import { parseMolScript } from 'mol-script/language/parser';
 import { transpileMolScript } from 'mol-script/script/mol-script/symbols';
+import { shapeFromPly } from 'mol-model-formats/shape/ply';
 
 export { TrajectoryFromBlob };
 export { TrajectoryFromMmCif };
@@ -338,7 +339,6 @@ function updateStructureFromQuery(query: QueryFn<Sel>, src: Structure, obj: SO.M
     return true;
 }
 
-
 namespace StructureComplexElement {
     export type Types = 'atomic-sequence' | 'water' | 'atomic-het' | 'spheres'
 }
@@ -394,4 +394,24 @@ async function attachProps(model: Model, ctx: PluginContext, taskCtx: RuntimeCon
         const p = ctx.customModelProperties.get(name);
         await p.attach(model).runInContext(taskCtx);
     }
-}
+}
+
+export { ShapeFromPly }
+type ShapeFromPly = typeof ShapeFromPly
+const ShapeFromPly = PluginStateTransform.BuiltIn({
+    name: 'shape-from-ply',
+    display: { name: 'Shape from PLY', description: 'Create Shape from PLY data' },
+    from: SO.Format.Ply,
+    to: SO.Shape.Provider,
+    params(a) {
+        return { };
+    }
+})({
+    apply({ a, params }) {
+        return Task.create('Create shape from PLY', async ctx => {
+            const shape = await shapeFromPly(a.data, params).runInContext(ctx)
+            const props = { label: 'Shape' };
+            return new SO.Shape.Provider(shape, props);
+        });
+    }
+});

+ 35 - 1
src/mol-plugin/state/transforms/representation.ts

@@ -30,6 +30,7 @@ import { Color } from 'mol-util/color';
 import { Overpaint } from 'mol-theme/overpaint';
 import { Transparency } from 'mol-theme/transparency';
 import { getStructureOverpaint, getStructureTransparency } from './helpers';
+import { BaseGeometry } from 'mol-geo/geometry/base';
 
 export { StructureRepresentation3D }
 export { StructureRepresentation3DHelpers }
@@ -193,7 +194,6 @@ const StructureRepresentation3D = PluginStateTransform.BuiltIn({
     }
 });
 
-
 type StructureLabels3D = typeof StructureLabels3D
 const StructureLabels3D = PluginStateTransform.BuiltIn({
     name: 'structure-labels-3d',
@@ -514,4 +514,38 @@ const VolumeRepresentation3D = PluginStateTransform.BuiltIn({
             return StateTransformer.UpdateResult.Updated;
         });
     }
+});
+
+//
+
+export { ShapeRepresentation3D }
+type ShapeRepresentation3D = typeof ShapeRepresentation3D
+const ShapeRepresentation3D = PluginStateTransform.BuiltIn({
+    name: 'shape-representation-3d',
+    display: '3D Representation',
+    from: SO.Shape.Provider,
+    to: SO.Shape.Representation3D,
+    params: (a, ctx: PluginContext) => {
+        return a ? a.data.params : BaseGeometry.Params
+    }
+})({
+    canAutoUpdate() {
+        return true;
+    },
+    apply({ a, params }, plugin: PluginContext) {
+        return Task.create('Shape Representation', async ctx => {
+            const props = { ...PD.getDefaultValues(a.data.params), params }
+            const repr = ShapeRepresentation(a.data.getShape, a.data.geometryUtils)
+            // TODO set initial state, repr.setState({})
+            await repr.createOrUpdate(props, a.data.data).runInContext(ctx);
+            return new SO.Shape.Representation3D({ repr, source: a }, { label: a.data.label });
+        });
+    },
+    update({ a, b, oldParams, newParams }, plugin: PluginContext) {
+        return Task.create('Shape Representation', async ctx => {
+            const props = { ...b.data.repr.props, ...newParams }
+            await b.data.repr.createOrUpdate(props, a.data.data).runInContext(ctx);
+            return StateTransformer.UpdateResult.Updated;
+        });
+    }
 });

+ 1 - 1
src/mol-plugin/util/structure-labels.ts

@@ -44,7 +44,7 @@ export async function getLabelRepresentation(ctx: RuntimeContext, structure: Str
 
 function getLabelsShape(ctx: RuntimeContext, data: LabelsData, props: PD.Values<Text.Params>, shape?: Shape<Text>) {
     const geo = getLabelsText(data, props, shape && shape.geometry);
-    return Shape.create('Scene Labels', geo, () => ColorNames.dimgrey, g => data.sizes[g], () => '')
+    return Shape.create('Scene Labels', data, geo, () => ColorNames.dimgrey, g => data.sizes[g], () => '')
 }
 
 const boundaryHelper = new BoundaryHelper();

+ 1 - 3
src/mol-repr/shape/representation.ts

@@ -57,9 +57,7 @@ export function ShapeRepresentation<D, G extends Geometry, P extends Geometry.Pa
             updateState.createNew = true
         } else if (shape && _shape && shape.id === _shape.id) {
             // console.log('same shape')
-            // trigger color update when shape has not changed
-            updateState.updateColor = true
-            updateState.updateTransform = true
+            // nothing to set
         } else if (shape && _shape && shape.id !== _shape.id) {
             // console.log('new shape')
             updateState.updateTransform = true

+ 5 - 1
src/mol-theme/color/secondary-structure.ts

@@ -22,6 +22,8 @@ const SecondaryStructureColors = ColorMap({
     'betaTurn': 0x6080FF,
     'betaStrand': 0xFFC800,
     'coil': 0xFFFFFF,
+    'bend': 0x66D8C9 /* biting original color used 0x00FF00 */,
+    'turn': 0x00B266,
 
     'dna': 0xAE00FE,
     'rna': 0xFD0162,
@@ -53,8 +55,10 @@ export function secondaryStructureColor(unit: Unit, element: ElementIndex): Colo
         return SecondaryStructureColors.alphaHelix
     } else if (SecondaryStructureType.is(secStrucType, SecondaryStructureType.Flag.Beta)) {
         return SecondaryStructureColors.betaStrand
+    } else if (SecondaryStructureType.is(secStrucType, SecondaryStructureType.Flag.Bend)) {
+        return SecondaryStructureColors.bend
     } else if (SecondaryStructureType.is(secStrucType, SecondaryStructureType.Flag.Turn)) {
-        return SecondaryStructureColors.coil
+        return SecondaryStructureColors.turn
     } else {
         const moleculeType = getElementMoleculeType(unit, element)
         if (moleculeType === MoleculeType.DNA) {

+ 6 - 6
src/mol-util/array.ts

@@ -1,5 +1,5 @@
 /**
- * Copyright (c) 2018 mol* contributors, licensed under MIT, See LICENSE file for more info.
+ * Copyright (c) 2018-2019 mol* contributors, licensed under MIT, See LICENSE file for more info.
  *
  * @author Alexander Rose <alexander.rose@weirdbyte.de>
  */
@@ -9,7 +9,7 @@ import { NumberArray } from './type-helpers';
 // TODO move to mol-math as Vector???
 
 /** Get the maximum value in an array */
-export function arrayMax(array: NumberArray) {
+export function arrayMax(array: ArrayLike<number>) {
     let max = -Infinity
     for (let i = 0, il = array.length; i < il; ++i) {
         if (array[i] > max) max = array[i]
@@ -18,7 +18,7 @@ export function arrayMax(array: NumberArray) {
 }
 
 /** Get the minimum value in an array */
-export function arrayMin(array: NumberArray) {
+export function arrayMin(array: ArrayLike<number>) {
     let min = Infinity
     for (let i = 0, il = array.length; i < il; ++i) {
         if (array[i] < min) min = array[i]
@@ -27,7 +27,7 @@ export function arrayMin(array: NumberArray) {
 }
 
 /** Get the sum of values in an array */
-export function arraySum(array: NumberArray, stride = 1, offset = 0) {
+export function arraySum(array: ArrayLike<number>, stride = 1, offset = 0) {
     const n = array.length
     let sum = 0
     for (let i = offset; i < n; i += stride) {
@@ -37,12 +37,12 @@ export function arraySum(array: NumberArray, stride = 1, offset = 0) {
 }
 
 /** Get the mean of values in an array */
-export function arrayMean(array: NumberArray, stride = 1, offset = 0) {
+export function arrayMean(array: ArrayLike<number>, stride = 1, offset = 0) {
     return arraySum(array, stride, offset) / (array.length / stride)
 }
 
 /** Get the root mean square of values in an array */
-export function arrayRms(array: NumberArray) {
+export function arrayRms(array: ArrayLike<number>) {
     const n = array.length
     let sumSq = 0
     for (let i = 0; i < n; ++i) {

+ 1 - 1
src/mol-util/param-definition.ts

@@ -284,7 +284,7 @@ export namespace ParamDefinition {
         return true;
     }
 
-    function isParamEqual(p: Any, a: any, b: any): boolean {
+    export function isParamEqual(p: Any, a: any, b: any): boolean {
         if (a === b) return true;
         if (!a) return !b;
         if (!b) return !a;

+ 34 - 35
src/tests/browser/index.html

@@ -1,38 +1,37 @@
 <!DOCTYPE html>
 <html lang="en">
-    <head>
-        <meta charset="utf-8" />
-        <meta name="viewport" content="width=device-width, user-scalable=no, minimum-scale=1.0, maximum-scale=1.0">
-        <title>Mol* Browser Test</title>
-        <style>
-            * {
-                margin: 0;
-                padding: 0;
-                box-sizing: border-box;
-            }
-            html, body {
-                width: 100%;
-                height: 100%;
-                overflow: hidden;
-            }
-        </style>
-    </head>
-    <body>
-        <div id="app"></div>
-        <script type="text/javascript">
-            function urlQueryParameter (id) {
-                if (typeof window === 'undefined') return undefined
-                const a = new RegExp(`${id}=([^&#=]*)`)
-                const m = a.exec(window.location.search)
-                return m ? decodeURIComponent(m[1]) : undefined
-            }
-
-            const name = urlQueryParameter('name')
-            if (name) {
-                const script = document.createElement('script')
-                script.src = name + '.js'
-                document.body.appendChild(script)
-            }
-        </script>
-    </body>
+	<head>
+		<meta charset="utf-8" />
+		<meta name="viewport" content="width=device-width, user-scalable=no, minimum-scale=1.0, maximum-scale=1.0">
+		<title>Mol* Browser Test</title>
+		<style>
+			* {
+				margin: 0;
+				padding: 0;
+				box-sizing: border-box;
+			}
+			html, body {
+				width: 100%;
+				height: 100%;
+				overflow: hidden;
+			}
+		</style>
+	</head>
+	<body>
+		<div id="app"></div>
+		<script type="text/javascript">
+			function urlQueryParameter (id) {
+				if (typeof window === 'undefined') return undefined
+				const a = new RegExp(`${id}=([^&#=]*)`)
+				const m = a.exec(window.location.search)
+				return m ? decodeURIComponent(m[1]) : undefined
+			}
+			const name = urlQueryParameter('name')
+			if (name) {
+				const script = document.createElement('script')
+				script.src = name + '.js'
+				document.body.appendChild(script)
+			}
+		</script>
+	</body>
 </html>

+ 6 - 7
src/tests/browser/render-shape.ts

@@ -68,7 +68,7 @@ async function getSphereMesh(ctx: RuntimeContext, centers: number[], mesh?: Mesh
     const builderState = MeshBuilder.createState(centers.length * 128, centers.length * 128 / 2, mesh)
     const t = Mat4.identity()
     const v = Vec3.zero()
-    const sphere = Sphere(2)
+    const sphere = Sphere(3)
     builderState.currentGroup = 0
     for (let i = 0, il = centers.length / 3; i < il; ++i) {
         // for production, calls to update should be guarded by `if (ctx.shouldUpdate)`
@@ -81,8 +81,8 @@ async function getSphereMesh(ctx: RuntimeContext, centers: number[], mesh?: Mesh
 }
 
 const myData = {
-    centers: [0, 0, 0, 0, 3, 0],
-    colors: [ColorNames.tomato, ColorNames.springgreen],
+    centers: [0, 0, 0, 0, 3, 0, 1, 0 , 4],
+    colors: [ColorNames.tomato, ColorNames.springgreen, ColorNames.springgreen],
     labels: ['Sphere 0, Instance A', 'Sphere 1, Instance A', 'Sphere 0, Instance B', 'Sphere 1, Instance B'],
     transforms: [Mat4.identity(), Mat4.fromTranslation(Mat4.zero(), Vec3.create(3, 0, 0))]
 }
@@ -96,8 +96,8 @@ async function getShape(ctx: RuntimeContext, data: MyData, props: {}, shape?: Sh
     const { centers, colors, labels, transforms } = data
     const mesh = await getSphereMesh(ctx, centers, shape && shape.geometry)
     const groupCount = centers.length / 3
-    return shape || Shape.create(
-        'test', mesh,
+    return Shape.create(
+        'test', data, mesh,
         (groupId: number) => colors[groupId], // color: per group, same for instances
         () => 1, // size: constant
         (groupId: number, instanceId: number) => labels[instanceId * groupCount + groupId], // label: per group and instance
@@ -108,10 +108,9 @@ async function getShape(ctx: RuntimeContext, data: MyData, props: {}, shape?: Sh
 // Init ShapeRepresentation container
 const repr = ShapeRepresentation(getShape, Mesh.Utils)
 
-async function init() {
+export async function init() {
     // Create shape from myData and add to canvas3d
     await repr.createOrUpdate({}, myData).run((p: Progress) => console.log(Progress.format(p)))
-    console.log(repr)
     canvas3d.add(repr)
     canvas3d.resetCamera()
 

Some files were not shown because too many files changed in this diff