UNPKG

@loaders.gl/ply

Version:

Framework-independent loader for the PLY format

801 lines (787 loc) 25.6 kB
(function webpackUniversalModuleDefinition(root, factory) { if (typeof exports === 'object' && typeof module === 'object') module.exports = factory(); else if (typeof define === 'function' && define.amd) define([], factory); else if (typeof exports === 'object') exports['loaders'] = factory(); else root['loaders'] = factory();})(globalThis, function () { "use strict"; var __exports__ = (() => { var __create = Object.create; var __defProp = Object.defineProperty; var __getOwnPropDesc = Object.getOwnPropertyDescriptor; var __getOwnPropNames = Object.getOwnPropertyNames; var __getProtoOf = Object.getPrototypeOf; var __hasOwnProp = Object.prototype.hasOwnProperty; var __commonJS = (cb, mod) => function __require() { return mod || (0, cb[__getOwnPropNames(cb)[0]])((mod = { exports: {} }).exports, mod), mod.exports; }; var __export = (target, all) => { for (var name in all) __defProp(target, name, { get: all[name], enumerable: true }); }; var __copyProps = (to, from, except, desc) => { if (from && typeof from === "object" || typeof from === "function") { for (let key of __getOwnPropNames(from)) if (!__hasOwnProp.call(to, key) && key !== except) __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable }); } return to; }; var __reExport = (target, mod, secondTarget) => (__copyProps(target, mod, "default"), secondTarget && __copyProps(secondTarget, mod, "default")); var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps( // If the importer is in node compatibility mode or this is not an ESM // file that has been converted to a CommonJS file using a Babel- // compatible transform (i.e. "__esModule" has not been set), then set // "default" to the CommonJS "module.exports" for node compatibility. isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target, mod )); var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod); // external-global-plugin:@loaders.gl/core var require_core = __commonJS({ "external-global-plugin:@loaders.gl/core"(exports, module) { module.exports = globalThis.loaders; } }); // bundle.ts var bundle_exports = {}; __export(bundle_exports, { PLYLoader: () => PLYLoader2, PLYWorkerLoader: () => PLYLoader }); __reExport(bundle_exports, __toESM(require_core(), 1)); // src/ply-loader.ts var VERSION = typeof __VERSION__ !== "undefined" ? __VERSION__ : "latest"; var PLYLoader = { dataType: null, batchType: null, name: "PLY", id: "ply", module: "ply", // shapes: ['mesh', 'gltf', 'columnar-table'], version: VERSION, worker: true, extensions: ["ply"], mimeTypes: ["text/plain", "application/octet-stream"], text: true, binary: true, tests: ["ply"], options: { ply: {} } }; // ../schema/src/lib/table/simple-table/data-type.ts function getDataTypeFromTypedArray(array) { switch (array.constructor) { case Int8Array: return "int8"; case Uint8Array: case Uint8ClampedArray: return "uint8"; case Int16Array: return "int16"; case Uint16Array: return "uint16"; case Int32Array: return "int32"; case Uint32Array: return "uint32"; case Float32Array: return "float32"; case Float64Array: return "float64"; default: return "null"; } } // ../schema/src/lib/mesh/mesh-utils.ts function getMeshBoundingBox(attributes) { let minX = Infinity; let minY = Infinity; let minZ = Infinity; let maxX = -Infinity; let maxY = -Infinity; let maxZ = -Infinity; const positions = attributes.POSITION ? attributes.POSITION.value : []; const len = positions && positions.length; for (let i = 0; i < len; i += 3) { const x = positions[i]; const y = positions[i + 1]; const z = positions[i + 2]; minX = x < minX ? x : minX; minY = y < minY ? y : minY; minZ = z < minZ ? z : minZ; maxX = x > maxX ? x : maxX; maxY = y > maxY ? y : maxY; maxZ = z > maxZ ? z : maxZ; } return [ [minX, minY, minZ], [maxX, maxY, maxZ] ]; } // ../schema/src/lib/mesh/deduce-mesh-schema.ts function deduceMeshSchema(attributes, metadata = {}) { const fields = deduceMeshFields(attributes); return { fields, metadata }; } function deduceMeshField(name, attribute, optionalMetadata) { const type = getDataTypeFromTypedArray(attribute.value); const metadata = optionalMetadata ? optionalMetadata : makeMeshAttributeMetadata(attribute); return { name, type: { type: "fixed-size-list", listSize: attribute.size, children: [{ name: "value", type }] }, nullable: false, metadata }; } function deduceMeshFields(attributes) { const fields = []; for (const attributeName in attributes) { const attribute = attributes[attributeName]; fields.push(deduceMeshField(attributeName, attribute)); } return fields; } function makeMeshAttributeMetadata(attribute) { const result = {}; if ("byteOffset" in attribute) { result.byteOffset = attribute.byteOffset.toString(10); } if ("byteStride" in attribute) { result.byteStride = attribute.byteStride.toString(10); } if ("normalized" in attribute) { result.normalized = attribute.normalized.toString(); } return result; } // src/lib/get-ply-schema.ts function getPLYSchema(plyHeader, attributes) { const metadata = makeMetadataFromPlyHeader(plyHeader); const schema = deduceMeshSchema(attributes, metadata); return schema; } function makeMetadataFromPlyHeader(plyHeader) { const metadata = {}; metadata.ply_comments = JSON.stringify(plyHeader.comments); metadata.ply_elements = JSON.stringify(plyHeader.elements); if (plyHeader.format !== void 0) { metadata.ply_format = plyHeader.format; } if (plyHeader.version !== void 0) { metadata.ply_version = plyHeader.version; } if (plyHeader.headerLength !== void 0) { metadata.ply_headerLength = plyHeader.headerLength.toString(10); } return metadata; } // src/lib/normalize-ply.ts function normalizePLY(plyHeader, plyAttributes, options) { const attributes = getMeshAttributes(plyAttributes); const boundingBox = getMeshBoundingBox(attributes); const vertexCount = plyAttributes.indices.length || plyAttributes.vertices.length / 3; const isTriangles = plyAttributes.indices && plyAttributes.indices.length > 0; const mode = isTriangles ? 4 : 0; const topology = isTriangles ? "triangle-list" : "point-list"; const schema = getPLYSchema(plyHeader, attributes); const plyMesh = { loader: "ply", loaderData: plyHeader, header: { vertexCount, boundingBox }, schema, attributes, indices: { value: new Uint32Array(0), size: 0 }, mode, topology }; if (plyAttributes.indices.length > 0) { plyMesh.indices = { value: new Uint32Array(plyAttributes.indices), size: 1 }; } return plyMesh; } function getMeshAttributes(attributes) { const accessors = {}; for (const attributeName of Object.keys(attributes)) { switch (attributeName) { case "vertices": if (attributes.vertices.length > 0) { accessors.POSITION = { value: new Float32Array(attributes.vertices), size: 3 }; } break; case "normals": if (attributes.normals.length > 0) { accessors.NORMAL = { value: new Float32Array(attributes.normals), size: 3 }; } break; case "uvs": if (attributes.uvs.length > 0) { accessors.TEXCOORD_0 = { value: new Float32Array(attributes.uvs), size: 2 }; } break; case "colors": if (attributes.colors.length > 0) { accessors.COLOR_0 = { value: new Uint8Array(attributes.colors), size: 3, normalized: true }; } break; case "indices": break; default: if (attributes[attributeName].length > 0) { accessors[attributeName] = { value: new Float32Array(attributes[attributeName]), size: 1 }; } break; } } return accessors; } // src/lib/parse-ply.ts 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); } 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; } function parseHeaderLines(lines, headerLength, options) { const header = { comments: [], elements: [], headerLength }; let lineType; let lineValues; let currentElement2 = null; for (let i = 0; i < lines.length; i++) { let 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 (currentElement2) { header.elements.push(currentElement2); } currentElement2 = { name: lineValues[0], count: parseInt(lineValues[1], 10), properties: [] }; break; case "property": if (currentElement2) { const property = makePLYElementProperty(lineValues); if (options?.propertyNameMapping && property.name in options?.propertyNameMapping) { property.name = options?.propertyNameMapping[property.name]; } currentElement2.properties.push(property); } break; default: console.log("unhandled", lineType, lineValues); } } if (currentElement2) { header.elements.push(currentElement2); } return header; } function getPLYAttributes(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: attributes[property.name] = []; break; } } } } return attributes; } 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] }; } } 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); } } 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; } function parseASCII(data, header) { 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 currentElement2 = 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[currentElement2].count) { currentElement2++; currentElementCount = 0; } const element = parsePLYElement(header.elements[currentElement2].properties, line); handleElement(attributes, header.elements[currentElement2].name, element); currentElementCount++; } } return attributes; } 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; 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]); } } } function binaryRead(dataview, at, type, littleEndian) { switch (type) { 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); } } 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); 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]; } 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 currentElement2 = 0; currentElement2 < header.elements.length; currentElement2++) { const count = header.elements[currentElement2].count; for (let currentElementCount = 0; currentElementCount < count; currentElementCount++) { result = binaryReadElement( body, loc, header.elements[currentElement2].properties, littleEndian ); loc += result[1]; const element = result[0]; handleElement(attributes, header.elements[currentElement2].name, element); } } return attributes; } // ../loader-utils/src/lib/iterators/text-iterators.ts async function* makeTextDecoderIterator(arrayBufferIterator, options = {}) { const textDecoder = new TextDecoder(void 0, options); for await (const arrayBuffer of arrayBufferIterator) { yield typeof arrayBuffer === "string" ? arrayBuffer : textDecoder.decode(arrayBuffer, { stream: true }); } } async function* makeLineIterator(textIterator) { let previous = ""; for await (const textChunk of textIterator) { previous += textChunk; let eolIndex; while ((eolIndex = previous.indexOf("\n")) >= 0) { const line = previous.slice(0, eolIndex + 1); previous = previous.slice(eolIndex + 1); yield line; } } if (previous.length > 0) { yield previous; } } // ../loader-utils/src/lib/iterators/async-iteration.ts async function forEach(iterator, visitor) { while (true) { const { done, value } = await iterator.next(); if (done) { iterator.return(); return; } const cancel = visitor(value); if (cancel) { return; } } } // src/lib/parse-ply-in-batches.ts var currentElement; async function* parsePLYInBatches(iterator, options) { const lineIterator = makeLineIterator(makeTextDecoderIterator(iterator)); const header = await parsePLYHeader(lineIterator, options); let attributes; switch (header.format) { case "ascii": attributes = await parseASCII2(lineIterator, header); break; default: throw new Error("Binary PLY can not yet be parsed in streaming mode"); } yield normalizePLY(header, attributes, options); } async function parsePLYHeader(lineIterator, options) { const header = { comments: [], elements: [] // headerLength }; await forEach(lineIterator, (line) => { line = line.trim(); if (line === "end_header") { return true; } if (line === "") { return false; } const lineValues = line.split(/\s+/); const lineType = lineValues.shift(); line = lineValues.join(" "); switch (lineType) { case "ply": 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 = makePLYElementProperty2(lineValues, options.propertyNameMapping); currentElement.properties.push(property); break; default: console.log("unhandled", lineType, lineValues); } return false; }); if (currentElement) { header.elements.push(currentElement); } return header; } function makePLYElementProperty2(propertyValues, propertyNameMapping) { 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] }; } } async function parseASCII2(lineIterator, header) { const attributes = { indices: [], vertices: [], normals: [], uvs: [], colors: [] }; let currentElement2 = 0; let currentElementCount = 0; for await (let line of lineIterator) { line = line.trim(); if (line !== "") { if (currentElementCount >= header.elements[currentElement2].count) { currentElement2++; currentElementCount = 0; } const element = parsePLYElement2(header.elements[currentElement2].properties, line); handleElement2(attributes, header.elements[currentElement2].name, element); currentElementCount++; } } return attributes; } function parseASCIINumber2(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); } } function parsePLYElement2(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 = parseASCIINumber2(values.shift(), properties[i].countType); for (let j = 0; j < n; j++) { list.push(parseASCIINumber2(values.shift(), properties[i].itemType)); } element[properties[i].name] = list; } else { element[properties[i].name] = parseASCIINumber2(values.shift(), properties[i].type); } } return element; } function handleElement2(buffer, elementName, element = {}) { 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, element.green / 255, element.blue / 255); } break; case "face": const vertexIndices = element.vertex_indices || element.vertex_index; 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; } } // src/index.ts var PLYLoader2 = { ...PLYLoader, // Note: parsePLY supports both text and binary parse: async (arrayBuffer, options) => parsePLY(arrayBuffer, options?.ply), // TODO - this may not detect text correctly? parseTextSync: (arrayBuffer, options) => parsePLY(arrayBuffer, options?.ply), parseSync: (arrayBuffer, options) => parsePLY(arrayBuffer, options?.ply), parseInBatches: (arrayBuffer, options) => parsePLYInBatches(arrayBuffer, options?.ply) }; return __toCommonJS(bundle_exports); })(); return __exports__; });