NGL@1.0.0-beta.7 Home Manual Reference Source Gallery

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