123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263 |
- /**
- * 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;
|