/** * Copyright (c) 2019 mol* contributors, licensed under MIT, See LICENSE file for more info. * * @author Alexander Rose */ 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>() 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> { 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>('Parse PLY', async ctx => { return await parseInternal(data, ctx) }) } export default parse;