Reference Source

src/parsers/parsePCDIntoXKTModel.js

/**
 * @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};