src/parser/ply-parser.js
/**
* @file Ply Parser
* @author Alexander Rose <alexander.rose@weirdbyte.de>
* @private
*/
import {
Geometry, Vector3, Face3, Color
} from '../../lib/three.es6.js'
import { ParserRegistry } from '../globals.js'
import SurfaceParser from './surface-parser.js'
/**
* PLYLoader
* @class
* @private
* @author Wei Meng / http://about.me/menway
*
* @description
* A THREE loader for PLY ASCII files (known as the Polygon File Format or the Stanford Triangle Format).
*
* Limitations: ASCII decoding assumes file is UTF-8.
*
* @example
* var loader = new THREE.PLYLoader();
* loader.load('./models/ply/ascii/dolphins.ply', function (geometry) {
* scene.add( new THREE.Mesh( geometry ) );
* } );
*
* // 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.
*
* loader.setPropertyNameMapping( {
* diffuse_red: 'red',
* diffuse_green: 'green',
* diffuse_blue: 'blue'
* } );
*
*/
function PLYLoader () {
this.propertyNameMapping = {}
}
PLYLoader.prototype = {
constructor: PLYLoader,
setPropertyNameMapping: function (mapping) {
this.propertyNameMapping = mapping
},
bin2str: function (buf) {
var arrayBuffer = new Uint8Array(buf)
var str = ''
for (var i = 0; i < buf.byteLength; i++) {
str += String.fromCharCode(arrayBuffer[ i ]) // implicitly assumes little-endian
}
return str
},
isASCII: function (data) {
var header = this.parseHeader(this.bin2str(data))
return header.format === 'ascii'
},
parse: function (data) {
if (data instanceof ArrayBuffer) {
return (
this.isASCII(data)
? this.parseASCII(this.bin2str(data))
: this.parseBinary(data)
)
} else {
return this.parseASCII(data)
}
},
parseHeader: function (data) {
var patternHeader = /ply([\s\S]*)end_header\s/
var headerText = ''
var headerLength = 0
var result = patternHeader.exec(data)
if (result !== null) {
headerText = result[ 1 ]
headerLength = result[ 0 ].length
}
var header = {
comments: [],
elements: [],
headerLength: headerLength
}
var lines = headerText.split('\n')
var currentElement, lineType, lineValues
function makePlyElementProperty (propertValues, propertyNameMapping) {
var property = {
type: propertValues[ 0 ]
}
if (property.type === 'list') {
property.name = propertValues[ 3 ]
property.countType = propertValues[ 1 ]
property.itemType = propertValues[ 2 ]
} else {
property.name = propertValues[ 1 ]
}
if (property.name in propertyNameMapping) {
property.name = propertyNameMapping[ property.name ]
}
return property
}
for (var i = 0; i < lines.length; i++) {
var line = lines[ i ]
line = line.trim()
if (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':
if (currentElement !== undefined) {
header.elements.push(currentElement)
}
currentElement = {}
currentElement.name = lineValues[ 0 ]
currentElement.count = parseInt(lineValues[ 1 ])
currentElement.properties = []
break
case 'property':
currentElement.properties.push(makePlyElementProperty(lineValues, this.propertyNameMapping))
break
default:
console.log('unhandled', lineType, lineValues)
}
}
if (currentElement !== undefined) {
header.elements.push(currentElement)
}
return header
},
parseASCIINumber: function (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)
case 'float': case 'double': case 'float32': case 'float64':
return parseFloat(n)
}
},
parseASCIIElement: function (properties, line) {
var values = line.split(/\s+/)
var element = {}
for (var i = 0; i < properties.length; i++) {
if (properties[ i ].type === 'list') {
var list = []
var n = this.parseASCIINumber(values.shift(), properties[ i ].countType)
for (var j = 0; j < n; j++) {
list.push(this.parseASCIINumber(values.shift(), properties[ i ].itemType))
}
element[ properties[ i ].name ] = list
} else {
element[ properties[ i ].name ] = this.parseASCIINumber(values.shift(), properties[ i ].type)
}
}
return element
},
parseASCII: function (data) {
// PLY ascii format specification, as per http://en.wikipedia.org/wiki/PLY_(file_format)
var geometry = new Geometry()
var result
var header = this.parseHeader(data)
var patternBody = /end_header\s([\s\S]*)$/
var body = ''
if ((result = patternBody.exec(data)) !== null) {
body = result[ 1 ]
}
var lines = body.split('\n')
var currentElement = 0
var currentElementCount = 0
geometry.useColor = false
for (var i = 0; i < lines.length; i++) {
var line = lines[ i ]
line = line.trim()
if (line === '') {
continue
}
if (currentElementCount >= header.elements[ currentElement ].count) {
currentElement++
currentElementCount = 0
}
var element = this.parseASCIIElement(header.elements[ currentElement ].properties, line)
this.handleElement(geometry, header.elements[ currentElement ].name, element)
currentElementCount++
}
return this.postProcess(geometry)
},
postProcess: function (geometry) {
if (geometry.useColor) {
for (var i = 0; i < geometry.faces.length; i++) {
geometry.faces[ i ].vertexColors = [
geometry.colors[ geometry.faces[ i ].a ],
geometry.colors[ geometry.faces[ i ].b ],
geometry.colors[ geometry.faces[ i ].c ]
]
}
geometry.elementsNeedUpdate = true
}
geometry.computeBoundingSphere()
return geometry
},
handleElement: function (geometry, elementName, element) {
if (elementName === 'vertex') {
geometry.vertices.push(
new Vector3(element.x, element.y, element.z)
)
if ('red' in element && 'green' in element && 'blue' in element) {
geometry.useColor = true
var color = new Color()
color.setRGB(element.red / 255.0, element.green / 255.0, element.blue / 255.0)
geometry.colors.push(color)
}
} else if (elementName === 'face') {
var vertexIndices = element.vertex_indices
if (vertexIndices.length === 3) {
geometry.faces.push(
new Face3(vertexIndices[ 0 ], vertexIndices[ 1 ], vertexIndices[ 2 ])
)
} else if (vertexIndices.length === 4) {
geometry.faces.push(
new Face3(vertexIndices[ 0 ], vertexIndices[ 1 ], vertexIndices[ 3 ]),
new Face3(vertexIndices[ 1 ], vertexIndices[ 2 ], vertexIndices[ 3 ])
)
}
}
},
binaryRead: function (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 ]
}
},
binaryReadElement: function (dataview, at, properties, littleEndian) {
var element = {}
var result
var read = 0
for (var i = 0; i < properties.length; i++) {
if (properties[ i ].type === 'list') {
var list = []
result = this.binaryRead(dataview, at + read, properties[ i ].countType, littleEndian)
var n = result[ 0 ]
read += result[ 1 ]
for (var j = 0; j < n; j++) {
result = this.binaryRead(dataview, at + read, properties[ i ].itemType, littleEndian)
list.push(result[ 0 ])
read += result[ 1 ]
}
element[ properties[ i ].name ] = list
} else {
result = this.binaryRead(dataview, at + read, properties[ i ].type, littleEndian)
element[ properties[ i ].name ] = result[ 0 ]
read += result[ 1 ]
}
}
return [ element, read ]
},
parseBinary: function (data) {
var geometry = new Geometry()
var header = this.parseHeader(this.bin2str(data))
var littleEndian = (header.format === 'binary_little_endian')
var body = new DataView(data, header.headerLength)
var result
var loc = 0
for (var currentElement = 0; currentElement < header.elements.length; currentElement++) {
for (var currentElementCount = 0; currentElementCount < header.elements[ currentElement ].count; currentElementCount++) {
result = this.binaryReadElement(body, loc, header.elements[ currentElement ].properties, littleEndian)
loc += result[ 1 ]
var element = result[ 0 ]
this.handleElement(geometry, header.elements[ currentElement ].name, element)
}
}
return this.postProcess(geometry)
}
}
class PlyParser extends SurfaceParser {
get type () { return 'ply' }
getLoader () {
return new PLYLoader()
}
}
ParserRegistry.add('ply', PlyParser)
export default PlyParser