UNPKG

@xeokit/xeokit-convert

Version:

JavaScript utilities to create .XKT files

298 lines (267 loc) 10.4 kB
/** * @desc Parses PCD point cloud data into an {@link XKTModel}. * * ## Usage * * In the example below we'll create an {@link XKTModel}, then load an LAZ point cloud model into it. * * ````javascript * utils.loadArraybuffer(""./models/pcd/ism_test_cat.pcd"", async (data) => { * * const xktModel = new XKTModel(); * * await parsePCDIntoXKTModel({ * data, * xktModel, * log: (msg) => { console.log(msg); } * }).then(()=>{ * xktModel.finalize(); * }, * (msg) => { * console.error(msg); * }); * }); * ```` * * @param {Object} params Parsing params. * @param {ArrayBuffer} params.data PCD file data. * @param {Boolean} [params.littleEndian=true] Whether PCD binary data is Little-Endian or Big-Endian. * @param {XKTModel} params.xktModel XKTModel to parse into. * @param {Object} [params.stats] Collects statistics. * @param {function} [params.log] Logging callback. @returns {Promise} Resolves when PCD has been parsed. */ function parsePCDIntoXKTModel({data, xktModel, littleEndian = true, stats, log}) { if (log) { log("Using parser: parsePCDIntoXKTModel"); } return new Promise(function(resolve, reject) { const textData = decodeText(new Uint8Array(data)); const header = parseHeader(textData); const positions = []; const normals = []; const colors = []; if (header.data === 'ascii') { const offset = header.offset; const data = textData.substr(header.headerLen); const lines = data.split('\n'); for (let i = 0, l = lines.length; i < l; i++) { if (lines[i] === '') { continue; } const line = lines[i].split(' '); if (offset.x !== undefined) { positions.push(parseFloat(line[offset.x])); positions.push(parseFloat(line[offset.y])); positions.push(parseFloat(line[offset.z])); } if (offset.rgb !== undefined) { const rgb = parseFloat(line[offset.rgb]); const r = (rgb >> 16) & 0x0000ff; const g = (rgb >> 8) & 0x0000ff; const b = (rgb >> 0) & 0x0000ff; colors.push(r, g, b, 255); } else { colors.push(255); colors.push(255); colors.push(255); } } } if (header.data === 'binary_compressed') { const sizes = new Uint32Array(data.slice(header.headerLen, header.headerLen + 8)); const compressedSize = sizes[0]; const decompressedSize = sizes[1]; const decompressed = decompressLZF(new Uint8Array(data, header.headerLen + 8, compressedSize), decompressedSize); const dataview = new DataView(decompressed.buffer); const offset = header.offset; for (let i = 0; i < header.points; i++) { if (offset.x !== undefined) { positions.push(dataview.getFloat32((header.points * offset.x) + header.size[0] * i, littleEndian)); positions.push(dataview.getFloat32((header.points * offset.y) + header.size[1] * i, littleEndian)); positions.push(dataview.getFloat32((header.points * offset.z) + header.size[2] * i, littleEndian)); } if (offset.rgb !== undefined) { colors.push(dataview.getUint8((header.points * offset.rgb) + header.size[3] * i + 0)); colors.push(dataview.getUint8((header.points * offset.rgb) + header.size[3] * i + 1)); colors.push(dataview.getUint8((header.points * offset.rgb) + header.size[3] * i + 2)); // colors.push(255); } else { colors.push(1); colors.push(1); colors.push(1); } } } if (header.data === 'binary') { const dataview = new DataView(data, header.headerLen); const offset = header.offset; for (let i = 0, row = 0; i < header.points; i++, row += header.rowSize) { if (offset.x !== undefined) { positions.push(dataview.getFloat32(row + offset.x, littleEndian)); positions.push(dataview.getFloat32(row + offset.y, littleEndian)); positions.push(dataview.getFloat32(row + offset.z, littleEndian)); } if (offset.rgb !== undefined) { colors.push(dataview.getUint8(row + offset.rgb + 2)); colors.push(dataview.getUint8(row + offset.rgb + 1)); colors.push(dataview.getUint8(row + offset.rgb + 0)); } else { colors.push(255); colors.push(255); colors.push(255); } } } xktModel.createGeometry({ geometryId: "pointsGeometry", primitiveType: "points", positions: positions, colors: colors && colors.length > 0 ? colors : null }); xktModel.createMesh({ meshId: "pointsMesh", geometryId: "pointsGeometry" }); xktModel.createEntity({ entityId: "geometries", meshIds: ["pointsMesh"] }); if (log) { log("Converted drawable objects: 1"); log("Converted geometries: 1"); log("Converted vertices: " + positions.length / 3); } if (stats) { stats.sourceFormat = "PCD"; stats.schemaVersion = ""; stats.title = ""; stats.author = ""; stats.created = ""; stats.numObjects = 1; stats.numGeometries = 1; stats.numVertices = positions.length / 3; } resolve(); }); } function parseHeader(data) { const header = {}; const result1 = data.search(/[\r\n]DATA\s(\S*)\s/i); const result2 = /[\r\n]DATA\s(\S*)\s/i.exec(data.substr(result1 - 1)); header.data = result2[1]; header.headerLen = result2[0].length + result1; header.str = data.substr(0, header.headerLen); header.str = header.str.replace(/\#.*/gi, ''); // Strip comments header.version = /VERSION (.*)/i.exec(header.str); // Parse header.fields = /FIELDS (.*)/i.exec(header.str); header.size = /SIZE (.*)/i.exec(header.str); header.type = /TYPE (.*)/i.exec(header.str); header.count = /COUNT (.*)/i.exec(header.str); header.width = /WIDTH (.*)/i.exec(header.str); header.height = /HEIGHT (.*)/i.exec(header.str); header.viewpoint = /VIEWPOINT (.*)/i.exec(header.str); header.points = /POINTS (.*)/i.exec(header.str); if (header.version !== null) { header.version = parseFloat(header.version[1]); } if (header.fields !== null) { header.fields = header.fields[1].split(' '); } if (header.type !== null) { header.type = header.type[1].split(' '); } if (header.width !== null) { header.width = parseInt(header.width[1]); } if (header.height !== null) { header.height = parseInt(header.height[1]); } if (header.viewpoint !== null) { header.viewpoint = header.viewpoint[1]; } if (header.points !== null) { header.points = parseInt(header.points[1], 10); } if (header.points === null) { header.points = header.width * header.height; } if (header.size !== null) { header.size = header.size[1].split(' ').map(function (x) { return parseInt(x, 10); }); } if (header.count !== null) { header.count = header.count[1].split(' ').map(function (x) { return parseInt(x, 10); }); } else { header.count = []; for (let i = 0, l = header.fields.length; i < l; i++) { header.count.push(1); } } header.offset = {}; let sizeSum = 0; for (let i = 0, l = header.fields.length; i < l; i++) { if (header.data === 'ascii') { header.offset[header.fields[i]] = i; } else { header.offset[header.fields[i]] = sizeSum; sizeSum += header.size[i] * header.count[i]; } } header.rowSize = sizeSum; // For binary only return header; } function decodeText(array) { if (typeof TextDecoder !== 'undefined') { return new TextDecoder().decode(array); } let s = ''; for (let i = 0, il = array.length; i < il; i++) { s += String.fromCharCode(array[i]); } try { return decodeURIComponent(escape(s)); } catch (e) { return s; } } function decompressLZF(inData, outLength) { // https://gitlab.com/taketwo/three-pcd-loader/blob/master/decompress-lzf.js const inLength = inData.length; const outData = new Uint8Array(outLength); let inPtr = 0; let outPtr = 0; let ctrl; let len; let ref; do { ctrl = inData[inPtr++]; if (ctrl < (1 << 5)) { ctrl++; if (outPtr + ctrl > outLength) throw new Error('Output buffer is not large enough'); if (inPtr + ctrl > inLength) throw new Error('Invalid compressed data'); do { outData[outPtr++] = inData[inPtr++]; } while (--ctrl); } else { len = ctrl >> 5; ref = outPtr - ((ctrl & 0x1f) << 8) - 1; if (inPtr >= inLength) throw new Error('Invalid compressed data'); if (len === 7) { len += inData[inPtr++]; if (inPtr >= inLength) throw new Error('Invalid compressed data'); } ref -= inData[inPtr++]; if (outPtr + len + 2 > outLength) throw new Error('Output buffer is not large enough'); if (ref < 0) throw new Error('Invalid compressed data'); if (ref >= outPtr) throw new Error('Invalid compressed data'); do { outData[outPtr++] = outData[ref++]; } while (--len + 2); } } while (inPtr < inLength); return outData; } export {parsePCDIntoXKTModel};