@loaders.gl/ply
Version:
Framework-independent loader for the PLY format
292 lines (255 loc) • 7.4 kB
text/typescript
// 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;
}
}