Browse Source

ply parser generalization

Alexander Rose 6 years ago
parent
commit
f4d019ac1b

+ 0 - 19
src/mol-geo/geometry/mesh/builder/triangle.ts

@@ -1,19 +0,0 @@
-/**
- * Copyright (c) 2018 mol* contributors, licensed under MIT, See LICENSE file for more info.
- *
- * @author Alexander Rose <alexander.rose@weirdbyte.de>
- */
-
-import {  Mat4 } from 'mol-math/linear-algebra';
-import { MeshBuilder } from '../mesh-builder';
-
-const tmpSphereMat = Mat4.identity()
-
-function getTriangle(vertices: number[], normals: number[], indices: number[]) {
-
-    return {vertices, normals, indices};
-}
-
-export function addTriangle(state: MeshBuilder.State, triangle_vertices: number[], triangle_normals: number[], triangle_indices: number[]) {
-    MeshBuilder.addPrimitive(state, tmpSphereMat, getTriangle( triangle_vertices, triangle_normals, triangle_indices))
-}

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

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

@@ -0,0 +1,78 @@
+/**
+ * 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
+`
+
+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
+        console.log('x', x.toArray())
+
+        const face = plyFile.getElement('face') as PlyList
+        if (!face) return
+        const f0 = face.value(0)
+        console.log('f0', f0)
+        const f1 = face.value(1)
+        console.log('f1', f1)
+    });
+});

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

+ 205 - 267
src/mol-io/reader/ply/parser.ts

@@ -1,320 +1,258 @@
 /**
  * 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 { Tokens, TokenBuilder, Tokenizer } from '../common/text/tokenizer'
-import * as Data from './schema'
-import{ ReaderResult } from '../result'
-import {Task, RuntimeContext, chunkedSubtask } from 'mol-task'
-import { parseInt as fastParseInt, parseFloat as fastParseFloat } from '../common/text/number-parser'
-
-const enum PlyTokenType {
-    Value = 0,
-    Comment = 1,
-    End = 2,
-    property = 3,
-    element = 4
-}
+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,
-
-    tokenType: PlyTokenType;
-    runtimeCtx: RuntimeContext,
-    tokens: Tokens[],
-
-    fieldCount: number,
-
-    columnCount: number,
-    propertyCount: number,
-    vertexCount: number,
-    currentVertex: number,
-    currentProperty: number,
-    currentFace: number,
-    currentFaceElement: number,
-    faceCount: number,
-    endHeader: number,
-
-    initialHead: string[],
-    properties: number[],
-    vertices: number[],
-    colors: number[],
-    normals: number[],
-    faces: number[],
-    propertyNames: string[],
-    check: string[],
-
-    commentCharCode: number,
-    propertyCharCode: number,
-    elementCharCode: number
+    data: string
+    tokenizer: Tokenizer
+    runtimeCtx: RuntimeContext
+
+    comments: string[]
+    elementSpecs: ElementSpec[]
+    elements: PlyElement[]
 }
 
-function State(data: string, runtimeCtx: RuntimeContext, opts: PlyOptions): State {
+function State(data: string, runtimeCtx: RuntimeContext): State {
     const tokenizer = Tokenizer(data)
     return {
         data,
         tokenizer,
-
-        tokenType: PlyTokenType.End,
         runtimeCtx,
-        tokens: [],
-
-        fieldCount: 0,
-
-        columnCount: 0,
-        propertyCount: 0,
-        vertexCount: 0,
-        currentVertex: 0,
-        currentProperty: 0,
-        currentFace: 0,
-        currentFaceElement: 0,
-        faceCount: 0,
-        endHeader: 0,
-
-        initialHead: [],
-        properties: [],
-        vertices: [],
-        colors: [],
-        normals: [],
-        faces: [],
-        propertyNames: [],
-        check: [],
-
-        commentCharCode: opts.comment.charCodeAt(0),
-        propertyCharCode: opts.property.charCodeAt(0),
-        elementCharCode: opts.element.charCodeAt(0)
-    };
-}
 
-/**
- * Eat everything until a delimiter (whitespace) or newline occurs.
- * Ignores whitespace at the end of the value, i.e. trim right.
- * Returns true when a newline occurs after the value.
- */
-function eatValue(state: Tokenizer) {
-    while (state.position < state.length) {
-        const c = state.data.charCodeAt(state.position);
-        ++state.position
-        switch (c) {
-            case 10:  // \n
-            case 13:  // \r
-                return true;
-            case 32: // ' ' Delimeter of ply is space (Unicode 32)
-                return true;
-            case 9:  // \t
-            case 32:  // ' '
-                break;
-            default:
-                ++state.tokenEnd;
-                break;
-        }
+        comments: [],
+        elementSpecs: [],
+        elements: []
     }
 }
 
-function eatLine (state: Tokenizer) {
-    while (state.position < state.length) {
-        const c = state.data.charCodeAt(state.position);
-        ++state.position
-        switch (c) {
-            case 10:  // \n
-            case 13:  // \r
-                return true;
-            case 9:  // \t
-                break;
-            default:
-                ++state.tokenEnd;
-                break;
-        }
-    }
-
+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 skipLine(state: Tokenizer) {
-    while (state.position < state.length) {
-        const c = state.data.charCodeAt(state.position);
-        if (c === 10 || c === 13) return  // \n or \r
-        ++state.position
-    }
-}
+function parseHeader(state: State) {
+    const { tokenizer, comments, elementSpecs } = state
 
-function getColumns(state: State, numberOfColumns: number) {
-    eatLine(state.tokenizer);
-    let tmp = Tokenizer.getTokenString(state.tokenizer)
-    let split = tmp.split(' ', numberOfColumns);
-    return split;
-}
+    markHeader(tokenizer)
+    const headerLines = Tokenizer.getTokenString(tokenizer).split(/\r?\n/)
 
-/**
- * Move to the next token.
- * Returns true when the current char is a newline, i.e. indicating a full record.
- */
-function moveNextInternal(state: State) {
-    const tokenizer = state.tokenizer
+    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'`)
 
-    if (tokenizer.position >= tokenizer.length) {
-        state.tokenType = PlyTokenType.End;
-        return true;
-    }
+    let currentName: string | undefined
+    let currentCount: number | undefined
+    let currentProperties: Property[] | undefined
 
-    tokenizer.tokenStart = tokenizer.position;
-    tokenizer.tokenEnd = tokenizer.position;
-    const c = state.data.charCodeAt(tokenizer.position);
-    switch (c) {
-        case state.commentCharCode:
-            state.tokenType = PlyTokenType.Comment;
-            skipLine(tokenizer);
-            break;
-        case state.propertyCharCode: // checks all line beginning with 'p'
-            state.check = getColumns(state, 3);
-            if (state.check[0] !== 'ply' && state.faceCount === 0) {
-                state.propertyNames.push(state.check[1]);
-                state.propertyNames.push(state.check[2]);
-                state.propertyCount++;
-            }
-            return;
-        case state.elementCharCode: // checks all line beginning with 'e'
-            state.check = getColumns(state, 3);
-            if (state.check[1] === 'vertex') state.vertexCount= Number(state.check[2]);
-            if (state.check[1] === 'face') state.faceCount = Number(state.check[2]);
-            if (state.check[0] === 'end_header') state.endHeader = 1;
-            return;
-        default:                    // for all the other lines
-            state.tokenType = PlyTokenType.Value;
-            let return_value = eatValue(tokenizer);
-
-            if (state.endHeader === 1) {
-                if (state.currentVertex < state.vertexCount) {
-                    // TODO the numbers are parsed twice
-                    state.properties[state.currentVertex * state.propertyCount + state.currentProperty] = Number(Tokenizer.getTokenString(state.tokenizer));
-                    if (state.currentProperty < 3) {
-                        state.vertices[state.currentVertex * 3 + state.currentProperty] = fastParseFloat(state.tokenizer.data, state.tokenizer.tokenStart, state.tokenizer.tokenEnd);
-                    }
-                    if (state.currentProperty >= 3 && state.currentProperty < 6) {
-                        state.colors[state.currentVertex * 3 + state.currentProperty - 3] = fastParseInt(state.tokenizer.data, state.tokenizer.tokenStart, state.tokenizer.tokenEnd);
-                    }
-                    if (state.currentProperty >= 6 && state.currentProperty < 9) {
-                        state.normals[state.currentVertex * 3 + state.currentProperty - 6] = fastParseFloat(state.tokenizer.data, state.tokenizer.tokenStart, state.tokenizer.tokenEnd);
-                    }
-                    state.currentProperty++;
-                    if (state.currentProperty === state.propertyCount) {
-                        state.currentProperty = 0;
-                        state.currentVertex++;
-                    }
-                    return return_value;
-                }
-                if (state.currentFace < state.faceCount && state.currentVertex === state.vertexCount) {
-                    state.faces[state.currentFace * 4 + state.currentFaceElement] = fastParseInt(state.tokenizer.data, state.tokenizer.tokenStart, state.tokenizer.tokenEnd);
-                    state.currentFaceElement++;
-                    if (state.currentProperty === 4) {
-                        state.currentFaceElement = 0;
-                        state.currentFace++;
-                    }
+    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
                 }
             }
-            return return_value;
+            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[]
+                })
+            }
+        }
     }
-}
 
-/**
- * Moves to the next non-comment token/line.
- * Returns true when the current char is a newline, i.e. indicating a full record.
- */
-function moveNext(state: State) {
-    let newRecord = moveNextInternal(state);
-    while (state.tokenType === PlyTokenType.Comment) { // skip comment lines (marco)
-        newRecord = moveNextInternal(state);
+    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')
+        }
     }
-    return newRecord
 }
 
-function readRecordsChunk(chunkSize: number, state: State) {
-    if (state.tokenType === PlyTokenType.End) return 0
-
-    moveNext(state);
-    const { tokens, tokenizer } = state;
-    let counter = 0;
-    while (state.tokenType === PlyTokenType.Value && counter < chunkSize) {
-        TokenBuilder.add(tokens[state.fieldCount % state.columnCount], tokenizer.tokenStart, tokenizer.tokenEnd);
-        ++state.fieldCount
-        moveNext(state);
-        ++counter;
+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)
     }
-    return counter;
-}
-
-function readRecordsChunks(state: State) {
-    return chunkedSubtask(state.runtimeCtx, 100000, state, readRecordsChunk,
-        (ctx, state) => ctx.update({ message: 'Parsing...', current: state.tokenizer.position, max: state.data.length }));
 }
 
-function addHeadEntry (state: State) {
-    const head = Tokenizer.getTokenString(state.tokenizer)
-    state.initialHead.push(head)
-    state.tokens.push(TokenBuilder.create(head, state.data.length / 80))
+function getColumnSchema(type: PlyType): Column.Schema {
+    switch (type) {
+        case 'char': case 'uchar':
+        case 'short': case 'ushort':
+        case 'int': case 'uint':
+            return Column.Schema.int
+        case 'float': case 'double':
+            return Column.Schema.float
+    }
 }
 
-function init(state: State) { // only for first two lines to get the format and the coding! (marco)
-    let newRecord = moveNext(state)
-    while (!newRecord) {  // newRecord is only true when a newline occurs (marco)
-        addHeadEntry(state)
-        newRecord = moveNext(state);
-    }
-    addHeadEntry(state)
-    newRecord = moveNext(state);
-    while (!newRecord) {
-        addHeadEntry(state)
-        newRecord = moveNext(state);
+function parseTableElement(state: State, spec: TableElementSpec) {
+    const { elements, tokenizer } = state
+    const { count, properties } = spec
+    const propertyCount = properties.length
+    const propertyNames: string[] = []
+    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)
     }
-    addHeadEntry(state)
-    if (state.initialHead[0] !== 'ply') {
-        console.log('ERROR: this is not a .ply file!')
-        throw new Error('this is not a .ply file!');
-        return 0;
-    }
-    if (state.initialHead[2] !== 'ascii') {
-        console.log('ERROR: only ASCII-DECODING is supported!');
-        throw new Error('only ASCII-DECODING is supported!');
-        return 0;
+
+    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)
+        }
     }
-    state.columnCount = state.initialHead.length
-    return 1;
-}
 
-async function handleRecords(state: State): Promise<Data.PlyData> {
-    if (!init(state)) {
-        console.log('ERROR: parsing file (PLY) failed!')
-        throw new Error('arsing file (PLY) failed!');
+    for (let i = 0, il = propertyCount; i < il; ++i) {
+        const { type, name } = properties[i]
+        const column = TokenColumn(propertyTokens[i], getColumnSchema(type))
+        propertyNames.push(name)
+        propertyColumns.set(name, column)
     }
-    await readRecordsChunks(state)
 
-    return Data.PlyData(state.vertexCount, state.faceCount, state.propertyCount, state.initialHead, state.propertyNames, state.properties, state.vertices, state.colors, state.normals, state.faces)
+    elements.push({
+        kind: 'table',
+        rowCount: count,
+        propertyNames,
+        getProperty: (name: string) => propertyColumns.get(name)
+    })
 }
 
-async function parseInternal(data: string, ctx: RuntimeContext, opts: PlyOptions): Promise<ReaderResult<Data.PlyFile>> {
-    const state = State(data, ctx, opts);
+function parseListElement(state: State, spec: ListElementSpec) {
+    const { elements, tokenizer } = state
+    const { count, property } = spec
 
-    ctx.update({ message: 'Parsing...', current: 0, max: data.length });
-    const PLYdata = await handleRecords(state)
-    const result = Data.PlyFile(PLYdata)
+    // initial tokens size assumes triangle index data
+    const tokens = TokenBuilder.create(tokenizer.data, count * 2 * 3)
 
-    return ReaderResult.success(result);
+    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,
+        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
+        }
+    })
 }
 
-interface PlyOptions {
-    comment: string;
-    property: string;
-    element: string;
+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, opts?: Partial<PlyOptions>) {
-    const completeOpts = Object.assign({}, { comment: 'c', property: 'p', element: 'e' }, opts)
-    return Task.create<ReaderResult<Data.PlyFile>>('Parse PLY', async ctx => {
-        return await parseInternal(data, ctx, completeOpts);
-    });
+export function parse(data: string) {
+    return Task.create<Result<PlyFile>>('Parse PLY', async ctx => {
+        return await parseInternal(data, ctx)
+    })
 }
 
 export default parse;

+ 52 - 32
src/mol-io/reader/ply/schema.ts

@@ -1,48 +1,68 @@
 /**
  * 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 { CifField as PlyColumn } from '../cif/data-model'
+import { Column } from 'mol-data/db';
 
-export { PlyColumn }
+// http://paulbourke.net/dataformats/ply/
+// https://en.wikipedia.org/wiki/PLY_(file_format)
 
-export interface PlyFile {
-    readonly name?: string,
-    readonly PLY_File: PlyData
+export const PlyTypeByteLength = {
+    'char': 1,
+    'uchar': 1,
+    'short': 2,
+    'ushort': 2,
+    'int': 4,
+    'uint': 4,
+    'float': 4,
+    'double': 8
 }
-
-export function PlyFile(PLY_File: PlyData, name?: string): PlyFile {
-    return { name, PLY_File };
+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 PlyData {
-    readonly vertexCount: number,
-    readonly faceCount: number,
-    readonly propertyCount: number,
-    readonly initialHead: ReadonlyArray<string>,
-    readonly propertyNames: ReadonlyArray<string>,
-    readonly properties: number[],
-    readonly vertices: number[],
-    readonly colors: number[],
-    readonly normals: number[],
-    readonly faces: number[],
+export interface PlyFile {
+    readonly comments: ReadonlyArray<string>
+    readonly elementNames: ReadonlyArray<string>
+    getElement(name: string): PlyElement | undefined
 }
 
-// TODO note, removed `faces: [...faces]` pattern as that copies the data which I assume was not intentional (alex)
-export function PlyData(vertexCount: number, faceCount: number, propertyCount: number, initialHead: string[], propertyNames: string[],  properties: number[],  vertices: number[],  colors: number[],  normals: number[], faces: number[]): PlyData {
+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 {
-        vertexCount,
-        faceCount,
-        propertyCount,
-        initialHead,
-        propertyNames,
-        properties,
-        vertices,
-        colors,
-        normals,
-        faces
+        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>
+    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,
+    value: (row: number) => PlyListValue
 }

+ 53 - 32
src/mol-model-formats/shape/ply.ts

@@ -6,53 +6,74 @@
  */
 
 import { RuntimeContext, Task } from 'mol-task';
-import { addTriangle } from 'mol-geo/geometry/mesh/builder/triangle';
 import { ShapeProvider } from 'mol-model/shape/provider';
 import { Color } from 'mol-util/color';
-import { PlyData, PlyFile } from 'mol-io/reader/ply/schema';
+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';
 
-async function getPlyMesh(ctx: RuntimeContext, centers: number[], normals: number[], faces: number[], mesh?: Mesh) {
-    const builderState = MeshBuilder.createState(faces.length, faces.length, mesh)
-    builderState.currentGroup = 0
-    for (let i = 0, il = faces.length/4; i < il; ++i) {
-        if (i % 10000 === 0 && ctx.shouldUpdate) await ctx.update({ current: i, max: il, message: `adding triangle ${i}` })
-        builderState.currentGroup = i
-
-        let triangle_vertices: number[];
-        let triangle_normals: number[];
-        let triangle_indices: number[];
-        triangle_vertices = [centers[faces[4*i+1]*3], centers[faces[4*i+1]*3+1], centers[faces[4*i+1]*3+2],
-                             centers[faces[4*i+2]*3], centers[faces[4*i+2]*3+1], centers[faces[4*i+2]*3+2],
-                             centers[faces[4*i+3]*3], centers[faces[4*i+3]*3+1], centers[faces[4*i+3]*3+2]];
-        triangle_normals = [ normals[faces[4*i+1]*3], normals[faces[4*i+1]*3+1], normals[faces[4*i+1]*3+2],
-                             normals[faces[4*i+2]*3], normals[faces[4*i+2]*3+1], normals[faces[4*i+2]*3+2],
-                             normals[faces[4*i+3]*3], normals[faces[4*i+3]*3+1], normals[faces[4*i+3]*3+2]];
-        triangle_indices = [0, 1, 2];
-        // console.log(triangle_vertices)
-        addTriangle(builderState, triangle_vertices, triangle_normals, triangle_indices)
+async function getPlyMesh(ctx: RuntimeContext, vertex: PlyTable, face: PlyList, mesh?: Mesh) {
+    const builderState = MeshBuilder.createState(face.rowCount, face.rowCount, 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')
+    if (!nx || !ny || !nz) throw new Error('missing normal properties')
+
+    const atomid = vertex.getProperty('atomid')
+    if (!atomid) throw new Error('missing atomid property')
+
+    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))
+    }
+
+    for (let i = 0, il = face.rowCount; i < il; ++i) {
+        if (i % 10000 === 0 && ctx.shouldUpdate) await ctx.update({ current: i, max: il, message: `adding face ${i}` })
+
+        const { entries } = face.value(i)
+        ChunkedArray.add3(indices, entries[0], entries[1], entries[2])
     }
     return MeshBuilder.getMesh(builderState);
 }
 
-async function getShape(ctx: RuntimeContext, parsedData: PlyData, props: {}, shape?: Shape<Mesh>) {
+async function getShape(ctx: RuntimeContext, plyFile: PlyFile, props: {}, shape?: Shape<Mesh>) {
     await ctx.update('async creation of shape from  myData')
-    const { vertices, normals, faces, colors, properties } = parsedData
-    const mesh = await getPlyMesh(ctx, vertices, normals, faces, shape && shape.geometry)
+
+    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 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(
         'test', mesh,
         (groupId: number) => {
-            return Color.fromRgb(
-                colors[faces[4 * groupId + 1] * 3 + 0],
-                colors[faces[4 * groupId + 1] * 3 + 1],
-                colors[faces[4 * groupId + 1] * 3 + 2]
-            )
+            return Color.fromRgb(red.value(groupId), green.value(groupId), blue.value(groupId))
         },
         () => 1, // size: constant
         (groupId: number) => {
-            return properties[parsedData.propertyCount * faces[4 * groupId + 1] + 10].toString()
+            return atomid.value(groupId).toString()
         }
     )
 }
@@ -63,11 +84,11 @@ export const PlyShapeParams = {
 export type PlyShapeParams = typeof PlyShapeParams
 
 export function shapeFromPly(source: PlyFile, params?: {}) {
-    return Task.create<ShapeProvider<PlyData, Mesh, PlyShapeParams>>('Parse Shape Data', async ctx => {
+    return Task.create<ShapeProvider<PlyFile, Mesh, PlyShapeParams>>('Parse Shape Data', async ctx => {
         console.log('source', source)
         return {
             label: 'Mesh',
-            data: source.PLY_File,
+            data: source,
             getShape,
             geometryUtils: Mesh.Utils
         }

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

@@ -198,7 +198,7 @@ const ParsePly = PluginStateTransform.BuiltIn({
         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.name || 'PLY Data' });
+            return new SO.Format.Ply(parsed.result, { label: parsed.result.comments[0] || 'PLY Data' });
         });
     }
 });