diff --git a/src/mol-geo/geometry/mesh/builder/triangle.ts b/src/mol-geo/geometry/mesh/builder/triangle.ts deleted file mode 100644 index fe0ba35d3b18d7e4bcaffae6646a0b88a33101da..0000000000000000000000000000000000000000 --- a/src/mol-geo/geometry/mesh/builder/triangle.ts +++ /dev/null @@ -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)) -} \ No newline at end of file diff --git a/src/mol-geo/geometry/mesh/mesh-builder.ts b/src/mol-geo/geometry/mesh/mesh-builder.ts index 2bcaf085159f4597b6ee0c0d915b754c6aeac46c..cfe57234db1d2b4fd8ce2648158cf4d4369e5656 100644 --- a/src/mol-geo/geometry/mesh/mesh-builder.ts +++ b/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]); diff --git a/src/mol-io/reader/_spec/ply.spec.ts b/src/mol-io/reader/_spec/ply.spec.ts new file mode 100644 index 0000000000000000000000000000000000000000..5427eeb2bc3acd5017618d35c2205a4a52fe5e9f --- /dev/null +++ b/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) + }); +}); \ No newline at end of file diff --git a/src/mol-io/reader/csv/data-model.ts b/src/mol-io/reader/csv/data-model.ts index 401c7aa2d5855cd530131ac22345bc0052823f9f..7c538467e25bf5eb2143ba569af1af41fe02e3e1 100644 --- a/src/mol-io/reader/csv/data-model.ts +++ b/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; } }; -// }; -// } \ No newline at end of file +export type CsvColumns = { [name: string]: CsvColumn } \ No newline at end of file diff --git a/src/mol-io/reader/ply/parser.ts b/src/mol-io/reader/ply/parser.ts index 96963639992da31e8730f92294c6d5d87ab2ee41..3ec43ccb94244b04e87618b100eaedcb65fa51d1 100644 --- a/src/mol-io/reader/ply/parser.ts +++ b/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; \ No newline at end of file diff --git a/src/mol-io/reader/ply/schema.ts b/src/mol-io/reader/ply/schema.ts index 4e4cd50552021156cf00b98bccc2de849f6dc498..6f37e32235ae80212f4bbea65feb85ca3bdaf8b8 100644 --- a/src/mol-io/reader/ply/schema.ts +++ b/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 } \ No newline at end of file diff --git a/src/mol-model-formats/shape/ply.ts b/src/mol-model-formats/shape/ply.ts index ffb3fe12db973f2a5555a44d3e72ba0ba3df471a..2b80c8230745a210cde528c5fb4455d22d111e45 100644 --- a/src/mol-model-formats/shape/ply.ts +++ b/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 } diff --git a/src/mol-plugin/state/transforms/data.ts b/src/mol-plugin/state/transforms/data.ts index d41daabb9e0f91b89ffe37419dd7aa5f1977d178..5769e684c89869f9d047ed6221daf63bd7b7d2b0 100644 --- a/src/mol-plugin/state/transforms/data.ts +++ b/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' }); }); } });