UNPKG

@loaders.gl/ply

Version:

Framework-independent loader for the PLY format

292 lines (255 loc) 7.4 kB
// PLY Loader, adapted from THREE.js (MIT license) // // Attributions per original THREE.js source file: // // @author Wei Meng / http://about.me/menway // // Description: A loader for PLY ASCII files (known as the Polygon File Format // or the Stanford Triangle Format). // // Limitations: ASCII decoding assumes file is UTF-8. // // If the PLY file uses non standard property names, they can be mapped while // loading. For example, the following maps the properties // “diffuse_(red|green|blue)” in the file to standard color names. // // parsePLY(data, { // propertyNameMapping: { // diffuse_red: 'red', // diffuse_green: 'green', // diffuse_blue: 'blue' // } // }); import {makeLineIterator, makeTextDecoderIterator, forEach} from '@loaders.gl/loader-utils'; import normalizePLY from './normalize-ply'; import {PLYMesh, PLYHeader, PLYElement, PLYProperty, PLYAttributes} from './ply-types'; let currentElement: PLYElement; /** * PARSER * @param iterator * @param options */ export async function* parsePLYInBatches( iterator: AsyncIterable<ArrayBuffer> | Iterable<ArrayBuffer>, options: any ): AsyncIterable<PLYMesh> { const lineIterator = makeLineIterator(makeTextDecoderIterator(iterator)); const header = await parsePLYHeader(lineIterator, options); let attributes: PLYAttributes; switch (header.format) { case 'ascii': attributes = await parseASCII(lineIterator, header); break; default: throw new Error('Binary PLY can not yet be parsed in streaming mode'); // attributes = await parseBinary(lineIterator, header); } yield normalizePLY(header, attributes, options); } /** * Parses header * @param lineIterator * @param options * @returns */ async function parsePLYHeader( lineIterator: AsyncIterable<string> | Iterable<string>, options: {[key: string]: any} ): Promise<PLYHeader> { const header: PLYHeader = { comments: [], elements: [] // headerLength }; // Note: forEach does not reset iterator if exiting loop prematurely // so that iteration can continue in a second loop await forEach(lineIterator, (line: string) => { line = line.trim(); // End of header if (line === 'end_header') { return true; // Returning true cancels `forEach` } // Ignore empty lines if (line === '') { // eslint-disable-next-line return false; // Returning false does not cancel `forEach` } const lineValues = line.split(/\s+/); const lineType = lineValues.shift(); line = lineValues.join(' '); switch (lineType) { case 'ply': // First line magic characters, ignore break; case 'format': header.format = lineValues[0]; header.version = lineValues[1]; break; case 'comment': header.comments.push(line); break; case 'element': if (currentElement) { header.elements.push(currentElement); } currentElement = { name: lineValues[0], count: parseInt(lineValues[1], 10), properties: [] }; break; case 'property': const property = makePLYElementProperty(lineValues, options.propertyNameMapping); currentElement.properties.push(property); break; default: // eslint-disable-next-line console.log('unhandled', lineType, lineValues); } return false; }); if (currentElement) { header.elements.push(currentElement); } return header; } function makePLYElementProperty(propertyValues: string[], propertyNameMapping: []): PLYProperty { const type = propertyValues[0]; switch (type) { case 'list': return { type, name: propertyValues[3], countType: propertyValues[1], itemType: propertyValues[2] }; default: return { type, name: propertyValues[1] }; } } // ASCII PARSING /** * @param lineIterator * @param header * @returns */ async function parseASCII(lineIterator: AsyncIterable<string>, header: PLYHeader) { // PLY ascii format specification, as per http://en.wikipedia.org/wiki/PLY_(file_format) const attributes: PLYAttributes = { indices: [], vertices: [], normals: [], uvs: [], colors: [] }; let currentElement = 0; let currentElementCount = 0; for await (let line of lineIterator) { line = line.trim(); if (line !== '') { if (currentElementCount >= header.elements[currentElement].count) { currentElement++; currentElementCount = 0; } const element = parsePLYElement(header.elements[currentElement].properties, line); handleElement(attributes, header.elements[currentElement].name, element); currentElementCount++; } } return attributes; } /** * Parses ASCII number * @param n * @param type * @returns ASCII number */ // eslint-disable-next-line complexity function parseASCIINumber(n: string, type: string): number { switch (type) { case 'char': case 'uchar': case 'short': case 'ushort': case 'int': case 'uint': case 'int8': case 'uint8': case 'int16': case 'uint16': case 'int32': case 'uint32': return parseInt(n, 10); case 'float': case 'double': case 'float32': case 'float64': return parseFloat(n); default: throw new Error(type); } } /** * Parses ASCII element * @param properties * @param line * @returns element */ function parsePLYElement(properties: any[], line: string) { const values: any = line.split(/\s+/); const element = {}; for (let i = 0; i < properties.length; i++) { if (properties[i].type === 'list') { const list: any = []; const n = parseASCIINumber(values.shift(), properties[i].countType); for (let j = 0; j < n; j++) { list.push(parseASCIINumber(values.shift(), properties[i].itemType)); } element[properties[i].name] = list; } else { element[properties[i].name] = parseASCIINumber(values.shift(), properties[i].type); } } return element; } /** * @param buffer * @param elementName * @param element */ // HELPER FUNCTIONS // eslint-disable-next-line complexity function handleElement( buffer: {[index: string]: number[]}, elementName: string, element: any = {} ) { switch (elementName) { case 'vertex': buffer.vertices.push(element.x, element.y, element.z); if ('nx' in element && 'ny' in element && 'nz' in element) { buffer.normals.push(element.nx, element.ny, element.nz); } if ('s' in element && 't' in element) { buffer.uvs.push(element.s, element.t); } if ('red' in element && 'green' in element && 'blue' in element) { buffer.colors.push(element.red / 255.0, element.green / 255.0, element.blue / 255.0); } break; case 'face': const vertexIndices = element.vertex_indices || element.vertex_index; // issue #9338 if (vertexIndices.length === 3) { buffer.indices.push(vertexIndices[0], vertexIndices[1], vertexIndices[2]); } else if (vertexIndices.length === 4) { buffer.indices.push(vertexIndices[0], vertexIndices[1], vertexIndices[3]); buffer.indices.push(vertexIndices[1], vertexIndices[2], vertexIndices[3]); } break; default: break; } }