Browse Source

Merge pull request #6 from molstar/ply-test

Ply test
MarcoSchaeferT 6 years ago
parent
commit
f9a13782b7

+ 4 - 4
src/mol-io/reader/ply/parser.ts

@@ -140,11 +140,11 @@ function parseElements(state: State) {
 
 function getColumnSchema(type: PlyType): Column.Schema {
     switch (type) {
-        case 'char': case 'uchar':
-        case 'short': case 'ushort':
-        case 'int': case 'uint':
+        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 'float': case 'double': case 'float32': case 'float64':
             return Column.Schema.float
     }
 }

+ 10 - 1
src/mol-io/reader/ply/schema.ts

@@ -17,7 +17,16 @@ export const PlyTypeByteLength = {
     'int': 4,
     'uint': 4,
     'float': 4,
-    'double': 8
+    '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))

+ 91 - 25
src/mol-model-formats/shape/ply.ts

@@ -13,9 +13,15 @@ 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';
 
-async function getPlyMesh(ctx: RuntimeContext, vertex: PlyTable, face: PlyList, mesh?: Mesh) {
-    const builderState = MeshBuilder.createState(face.rowCount, face.rowCount, mesh)
+// TODO support 'edge' and 'material' elements, see https://www.mathworks.com/help/vision/ug/the-ply-format.html
+
+async function getPlyMesh(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')
@@ -26,17 +32,15 @@ async function getPlyMesh(ctx: RuntimeContext, vertex: PlyTable, face: PlyList,
     const nx = vertex.getProperty('nx')
     const ny = vertex.getProperty('ny')
     const nz = vertex.getProperty('nz')
-    if (!nx || !ny || !nz) throw new Error('missing normal properties')
 
-    const atomid = vertex.getProperty('atomid')
-    if (!atomid) throw new Error('missing atomid property')
+    const hasNormals = !!nx && !!ny && !!nz
 
     for (let i = 0, il = vertex.rowCount; i < il; ++i) {
         if (i % 10000 === 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))
-        ChunkedArray.add3(normals, nx.value(i), ny.value(i), nz.value(i));
-        ChunkedArray.add(groups, atomid.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) {
@@ -45,52 +49,114 @@ async function getPlyMesh(ctx: RuntimeContext, vertex: PlyTable, face: PlyList,
         const { entries } = face.value(i)
         ChunkedArray.add3(indices, entries[0], entries[1], entries[2])
     }
-    return MeshBuilder.getMesh(builderState);
+
+    const m = MeshBuilder.getMesh(builderState);
+    m.normalsComputed = hasNormals
+    await Mesh.computeNormals(m).runInContext(ctx)
+
+    return m
+}
+
+function getGrouping(count: number, column?: Column<number>) {
+    const ids = column ? column.toArray({ array: Int32Array }) : fillSerial(new Uint32Array(count))
+    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 }
 }
 
-async function getShape(ctx: RuntimeContext, plyFile: PlyFile, props: {}, shape?: Shape<Mesh>) {
-    await ctx.update('async creation of shape from  myData')
+async function getShape(ctx: RuntimeContext, plyFile: PlyFile, props: PD.Values<PlyShapeParams>, shape?: Shape<Mesh>) {
+    await ctx.update('async creation of shape from ply file')
+
+    const { coloring, grouping } = props
 
     const vertex = plyFile.getElement('vertex') as PlyTable
     if (!vertex) throw new Error('missing vertex element')
 
-    const atomid = vertex.getProperty('atomid')
-    if (!atomid) throw new Error('missing atomid property')
-
-    const red = vertex.getProperty('red')
-    const green = vertex.getProperty('green')
-    const blue = vertex.getProperty('blue')
-    if (!red || !green || !blue) throw new Error('missing color properties')
+    const { rowCount } = vertex
+    const int = Column.Schema.int
+
+    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 {
+        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)
+    }
 
     const face = plyFile.getElement('face') as PlyList
     if (!face) throw new Error('missing face element')
 
-    const mesh = await getPlyMesh(ctx, vertex, face, shape && shape.geometry)
-    return shape || Shape.create(
+    const { ids, map } = getGrouping(vertex.rowCount, grouping.name === 'vertex' ? vertex.getProperty(grouping.params.group) : undefined)
+
+    const mesh = await getPlyMesh(ctx, vertex, face, ids, shape && shape.geometry)
+    return Shape.create(
 
         'test', plyFile, mesh,
 
         (groupId: number) => {
-            return Color.fromRgb(red.value(groupId), green.value(groupId), blue.value(groupId))
+            const idx = map[groupId]
+            return Color.fromRgb(red.value(idx), green.value(idx), blue.value(idx))
         },
         () => 1, // size: constant
         (groupId: number) => {
-            return atomid.value(groupId).toString()
+            return ids[groupId].toString()
         }
     )
 }
 
-export const PlyShapeParams = {
-    ...Mesh.Params
+function createPlyShapeParams(vertex?: PlyTable) {
+    const options: [string, string][] = [['', '']]
+    const defaultValues = { group: '', red: '', green: '', blue: '' }
+    if (vertex) {
+        for (let i = 0, il = vertex.propertyNames.length; i < il; ++i) {
+            const name = vertex.propertyNames[i]
+            options.push([ name, name ])
+        }
+
+        // TODO harcoded as convenience for data provided by MegaMol
+        if (vertex.propertyNames.includes('atomid')) defaultValues.group = 'atomid'
+
+        if (vertex.propertyNames.includes('red')) defaultValues.red = 'red'
+        if (vertex.propertyNames.includes('green')) defaultValues.green = 'green'
+        if (vertex.propertyNames.includes('blue')) defaultValues.blue = 'blue'
+    }
+
+    return {
+        ...Mesh.Params,
+
+        coloring: PD.MappedStatic(defaultValues.red && defaultValues.green && defaultValues.blue ? 'vertex' : 'uniform', {
+            vertex: PD.Group({
+                red: PD.Select(defaultValues.red, options, { label: 'Red Property' }),
+                green: PD.Select(defaultValues.green, options, { label: 'Green Property' }),
+                blue: PD.Select(defaultValues.blue, options, { 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, options, { label: 'Group Property' }),
+            }, { isFlat: true }),
+            none: PD.Group({ })
+        }),
+    }
 }
+
+export const PlyShapeParams = createPlyShapeParams()
 export type PlyShapeParams = typeof PlyShapeParams
 
 export function shapeFromPly(source: PlyFile, params?: {}) {
-    return Task.create<ShapeProvider<PlyFile, Mesh, PlyShapeParams>>('Parse Shape Data', async ctx => {
-        console.log('source', source)
+    return Task.create<ShapeProvider<PlyFile, Mesh, PlyShapeParams>>('Shape Provider', async ctx => {
         return {
             label: 'Mesh',
             data: source,
+            params: createPlyShapeParams(source.getElement('vertex') as PlyTable),
             getShape,
             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);

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

@@ -10,6 +10,7 @@ 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>
 }

+ 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)

+ 1 - 1
src/mol-plugin/state/transforms/data.ts

@@ -190,7 +190,7 @@ export { ParsePly }
 type ParsePly = typeof ParsePly
 const ParsePly = PluginStateTransform.BuiltIn({
     name: 'parse-ply',
-    display: { name: 'Parse PLY', description: 'Parse PLY from Binary data' },
+    display: { name: 'Parse PLY', description: 'Parse PLY from String data' },
     from: [SO.Data.String],
     to: SO.Format.Ply
 })({

+ 0 - 1
src/mol-plugin/state/transforms/model.ts

@@ -339,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'
 }

+ 4 - 2
src/mol-plugin/state/transforms/representation.ts

@@ -474,6 +474,8 @@ const VolumeRepresentation3D = PluginStateTransform.BuiltIn({
     }
 });
 
+//
+
 export { ShapeRepresentation3D }
 type ShapeRepresentation3D = typeof ShapeRepresentation3D
 const ShapeRepresentation3D = PluginStateTransform.BuiltIn({
@@ -482,7 +484,7 @@ const ShapeRepresentation3D = PluginStateTransform.BuiltIn({
     from: SO.Shape.Provider,
     to: SO.Shape.Representation3D,
     params: (a, ctx: PluginContext) => {
-        return BaseGeometry.Params
+        return a ? a.data.params : BaseGeometry.Params
     }
 })({
     canAutoUpdate() {
@@ -490,7 +492,7 @@ const ShapeRepresentation3D = PluginStateTransform.BuiltIn({
     },
     apply({ a, params }, plugin: PluginContext) {
         return Task.create('Shape Representation', async ctx => {
-            const props = { ...PD.getDefaultValues(a.data.geometryUtils.Params), params }
+            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);

+ 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) {