@loaders.gl/ply
Version:
Framework-independent loader for the PLY format
400 lines (399 loc) • 12.5 kB
JavaScript
import normalizePLY from "./normalize-ply.js";
/**
* @param data
* @param options
* @returns
*/
export function parsePLY(data, options = {}) {
let header;
let attributes;
if (data instanceof ArrayBuffer) {
const text = new TextDecoder().decode(data);
header = parseHeader(text, options);
attributes = header.format === 'ascii' ? parseASCII(text, header) : parseBinary(data, header);
}
else {
header = parseHeader(data, options);
attributes = parseASCII(data, header);
}
return normalizePLY(header, attributes);
}
/**
* @param data
* @param options
* @returns header
*/
function parseHeader(data, options) {
const PLY_HEADER_PATTERN = /ply([\s\S]*)end_header\s/;
let headerText = '';
let headerLength = 0;
const result = PLY_HEADER_PATTERN.exec(data);
if (result !== null) {
headerText = result[1];
headerLength = result[0].length;
}
const lines = headerText.split('\n');
const header = parseHeaderLines(lines, headerLength, options);
return header;
}
/**
* @param lines
* @param headerLength
* @param options
* @returns header
*/
// eslint-disable-next-line complexity
function parseHeaderLines(lines, headerLength, options) {
const header = {
comments: [],
elements: [],
headerLength
};
let lineType;
let lineValues;
let currentElement = null;
for (let i = 0; i < lines.length; i++) {
let line = lines[i];
line = line.trim();
if (line === '') {
// eslint-disable-next-line
continue;
}
lineValues = line.split(/\s+/);
lineType = lineValues.shift();
line = lineValues.join(' ');
switch (lineType) {
case 'format':
header.format = lineValues[0];
header.version = lineValues[1];
break;
case 'comment':
header.comments.push(line);
break;
case 'element':
// Start new element, store previous element
if (currentElement) {
header.elements.push(currentElement);
}
currentElement = {
name: lineValues[0],
count: parseInt(lineValues[1], 10),
properties: []
};
break;
case 'property':
if (currentElement) {
const property = makePLYElementProperty(lineValues);
if (options?.propertyNameMapping && property.name in options?.propertyNameMapping) {
property.name = options?.propertyNameMapping[property.name];
}
currentElement.properties.push(property);
}
break;
default:
// eslint-disable-next-line
console.log('unhandled', lineType, lineValues);
}
}
// Store in-progress element
if (currentElement) {
header.elements.push(currentElement);
}
return header;
}
/** Generate attributes arrays from the header */
// eslint-disable-next-line complexity
function getPLYAttributes(header) {
// TODO Generate only the attribute arrays actually in the header
const attributes = {
indices: [],
vertices: [],
normals: [],
uvs: [],
colors: []
};
for (const element of header.elements) {
if (element.name === 'vertex') {
for (const property of element.properties) {
switch (property.name) {
case 'x':
case 'y':
case 'z':
case 'nx':
case 'ny':
case 'nz':
case 's':
case 't':
case 'red':
case 'green':
case 'blue':
break;
default:
// Add any non-geometry attributes
attributes[property.name] = [];
break;
}
}
}
}
return attributes;
}
/**
* @param propertyValues
* @returns property of ply element
*/
function makePLYElementProperty(propertyValues) {
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]
};
}
}
/**
* Parses ASCII number
* @param n
* @param type
* @returns
*/
// eslint-disable-next-line complexity
function parseASCIINumber(n, type) {
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);
}
}
/**
* @param properties
* @param line
* @returns ASCII element
*/
function parsePLYElement(properties, line) {
const values = line.split(/\s+/);
const element = {};
for (let i = 0; i < properties.length; i++) {
if (properties[i].type === 'list') {
const list = [];
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 data
* @param header
* @returns [attributes]
*/
function parseASCII(data, header) {
// PLY ascii format specification, as per http://en.wikipedia.org/wiki/PLY_(file_format)
const attributes = getPLYAttributes(header);
let result;
const patternBody = /end_header\s([\s\S]*)$/;
let body = '';
if ((result = patternBody.exec(data)) !== null) {
body = result[1];
}
const lines = body.split('\n');
let currentElement = 0;
let currentElementCount = 0;
for (let i = 0; i < lines.length; i++) {
let line = lines[i];
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;
}
/**
* @param buffer
* @param elementName
* @param element
*/
// eslint-disable-next-line complexity
function handleElement(buffer, elementName, element = {}) {
if (elementName === 'vertex') {
for (const propertyName of Object.keys(element)) {
switch (propertyName) {
case 'x':
buffer.vertices.push(element.x, element.y, element.z);
break;
case 'y':
case 'z':
break;
case 'nx':
if ('nx' in element && 'ny' in element && 'nz' in element) {
buffer.normals.push(element.nx, element.ny, element.nz);
}
break;
case 'ny':
case 'nz':
break;
case 's':
if ('s' in element && 't' in element) {
buffer.uvs.push(element.s, element.t);
}
break;
case 't':
break;
case 'red':
if ('red' in element && 'green' in element && 'blue' in element) {
buffer.colors.push(element.red, element.green, element.blue);
}
break;
case 'green':
case 'blue':
break;
default:
buffer[propertyName].push(element[propertyName]);
}
}
}
else if (elementName === '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]);
}
}
}
/**
* Reads binary data
* @param dataview
* @param at
* @param type
* @param littleEndian
* @returns [number, number]
*/
// eslint-disable-next-line complexity
function binaryRead(dataview, at, type, littleEndian) {
switch (type) {
// corespondences for non-specific length types here match rply:
case 'int8':
case 'char':
return [dataview.getInt8(at), 1];
case 'uint8':
case 'uchar':
return [dataview.getUint8(at), 1];
case 'int16':
case 'short':
return [dataview.getInt16(at, littleEndian), 2];
case 'uint16':
case 'ushort':
return [dataview.getUint16(at, littleEndian), 2];
case 'int32':
case 'int':
return [dataview.getInt32(at, littleEndian), 4];
case 'uint32':
case 'uint':
return [dataview.getUint32(at, littleEndian), 4];
case 'float32':
case 'float':
return [dataview.getFloat32(at, littleEndian), 4];
case 'float64':
case 'double':
return [dataview.getFloat64(at, littleEndian), 8];
default:
throw new Error(type);
}
}
/**
* Reads binary data
* @param dataview
* @param at
* @param properties
* @param littleEndian
* @returns [object, number]
*/
function binaryReadElement(dataview, at, properties, littleEndian) {
const element = {};
let result;
let read = 0;
for (let i = 0; i < properties.length; i++) {
if (properties[i].type === 'list') {
const list = [];
result = binaryRead(dataview, at + read, properties[i].countType, littleEndian);
const n = result[0];
read += result[1];
for (let j = 0; j < n; j++) {
result = binaryRead(dataview, at + read, properties[i].itemType, littleEndian);
// @ts-ignore
list.push(result[0]);
read += result[1];
}
element[properties[i].name] = list;
}
else {
result = binaryRead(dataview, at + read, properties[i].type, littleEndian);
element[properties[i].name] = result[0];
read += result[1];
}
}
return [element, read];
}
/**
* Parses binary data
* @param data
* @param header
* @returns [attributes] of data
*/
function parseBinary(data, header) {
const attributes = getPLYAttributes(header);
const littleEndian = header.format === 'binary_little_endian';
const body = new DataView(data, header.headerLength);
let result;
let loc = 0;
for (let currentElement = 0; currentElement < header.elements.length; currentElement++) {
const count = header.elements[currentElement].count;
for (let currentElementCount = 0; currentElementCount < count; currentElementCount++) {
result = binaryReadElement(body, loc, header.elements[currentElement].properties, littleEndian);
loc += result[1];
const element = result[0];
handleElement(attributes, header.elements[currentElement].name, element);
}
}
return attributes;
}