Reference Source

src/convert2xkt.js

import {XKT_INFO} from "./XKT_INFO.js";
import {XKTModel} from "./XKTModel/XKTModel.js";
import {parseCityJSONIntoXKTModel} from "./parsers/parseCityJSONIntoXKTModel.js";
import {parseGLTFIntoXKTModel} from "./parsers/parseGLTFIntoXKTModel.js";
import {parseIFCIntoXKTModel} from "./parsers/parseIFCIntoXKTModel.js";
import {parseLASIntoXKTModel} from "./parsers/parseLASIntoXKTModel.js";
import {parsePCDIntoXKTModel} from "./parsers/parsePCDIntoXKTModel.js";
import {parsePLYIntoXKTModel} from "./parsers/parsePLYIntoXKTModel.js";
import {parseSTLIntoXKTModel} from "./parsers/parseSTLIntoXKTModel.js";
import {writeXKTModelToArrayBuffer} from "./XKTModel/writeXKTModelToArrayBuffer.js";

import {toArrayBuffer} from "./XKTModel/lib/toArraybuffer";

const fs = require('fs');
const path = require("path");

/**
 * Converts model files into xeokit's native XKT format.
 *
 * Supported source formats are: IFC, CityJSON, glTF, LAZ and LAS.
 *
 * **Only bundled in xeokit-convert.cjs.js.**
 *
 * ## Usage
 *
 * ````javascript
 * const convert2xkt = require("@xeokit/xeokit-convert/dist/convert2xkt.cjs.js");
 * const fs = require('fs');
 *
 * convert2xkt({
 *      sourceData: fs.readFileSync("rme_advanced_sample_project.ifc"),
 *      outputXKT: (xtkArrayBuffer) => {
 *          fs.writeFileSync("rme_advanced_sample_project.ifc.xkt", xtkArrayBuffer);
 *      }
 *  }).then(() => {
 *      console.log("Converted.");
 *  }, (errMsg) => {
 *      console.error("Conversion failed: " + errMsg)
 *  });
 ````
 * @param {Object} params Conversion parameters.
 * @param {Object} params.WebIFC The WebIFC library. We pass this in as an external dependency, in order to give the
 * caller the choice of whether to use the Browser or NodeJS version.
 * @param {*} [params.configs] Configurations.
 * @param {String} [params.source] Path to source file. Alternative to ````sourceData````.
 * @param {ArrayBuffer|JSON} [params.sourceData] Source file data. Alternative to ````source````.
 * @param {String} [params.sourceFormat] Format of source file/data. Always needed with ````sourceData````, but not normally needed with ````source````, because convert2xkt will determine the format automatically from the file extension of ````source````.
 * @param {String} [params.metaModelDataStr] Source file data. Overrides metadata from ````metaModelSource````, ````sourceData```` and ````source````.
 * @param {String} [params.metaModelSource] Path to source metaModel file. Overrides metadata from ````sourceData```` and ````source````. Overridden by ````metaModelData````.
 * @param {String} [params.output] Path to destination XKT file. Directories on this path are automatically created if not existing.
 * @param {Function} [params.outputXKTModel] Callback to collect the ````XKTModel```` that is internally build by this method.
 * @param {Function} [params.outputXKT] Callback to collect XKT file data.
 * @param {String[]} [params.includeTypes] Option to only convert objects of these types.
 * @param {String[]} [params.excludeTypes] Option to never convert objects of these types.
 * @param {Object} [stats] Collects conversion statistics. Statistics are attached to this object if provided.
 * @param {Function} [params.outputStats] Callback to collect statistics.
 * @param {Boolean} [params.rotateX=false] Whether to rotate the model 90 degrees about the X axis to make the Y axis "up", if necessary. Applies to CityJSON and LAS/LAZ models.
 * @param {Boolean} [params.reuseGeometries=true] When true, will enable geometry reuse within the XKT. When false,
 * will automatically "expand" all reused geometries into duplicate copies. This has the drawback of increasing the XKT
 * file size (~10-30% for typical models), but can make the model more responsive in the xeokit Viewer, especially if the model
 * has excessive geometry reuse. An example of excessive geometry reuse would be when a model (eg. glTF) has 4000 geometries that are
 * shared amongst 2000 objects, ie. a large number of geometries with a low amount of reuse, which can present a
 * pathological performance case for xeokit's underlying graphics APIs (WebGL, WebGPU etc).
 * @param {Boolean} [params.includeTextures=true] Whether to convert textures. Only works for ````glTF```` models.
 * @param {Boolean} [params.includeNormals=true] Whether to convert normals. When false, the parser will ignore
 * geometry normals, and the modelwill rely on the xeokit ````Viewer```` to automatically generate them. This has
 * the limitation that the normals will be face-aligned, and therefore the ````Viewer```` will only be able to render
 * a flat-shaded non-PBR representation of the model.
 * @param {Number} [params.minTileSize=200] Minimum RTC coordinate tile size. Set this to a value between 100 and 10000,
 * depending on how far from the coordinate origin the model's vertex positions are; specify larger tile sizes when close
 * to the origin, and smaller sizes when distant.  This compensates for decreasing precision as floats get bigger.
 * @param {Function} [params.log] Logging callback.
 * @return {Promise<number>}
 */
function convert2xkt({
                         WebIFC,
                         configs = {},
                         source,
                         sourceData,
                         sourceFormat,
                         metaModelSource,
                         metaModelDataStr,
                         modelAABB,
                         output,
                         outputXKTModel,
                         outputXKT,
                         includeTypes,
                         excludeTypes,
                         reuseGeometries = true,
                         minTileSize = 200,
                         stats = {},
                         outputStats,
                         rotateX = false,
                         includeTextures = true,
                         includeNormals = true,
                         zip = true,
                         log = function (msg) {
                         }
                     }) {

    stats.sourceFormat = "";
    stats.schemaVersion = "";
    stats.title = "";
    stats.author = "";
    stats.created = "";
    stats.numMetaObjects = 0;
    stats.numPropertySets = 0;
    stats.numTriangles = 0;
    stats.numVertices = 0;
    stats.numNormals = 0;
    stats.numUVs = 0;
    stats.numTextures = 0;
    stats.numTextureSets = 0;
    stats.numObjects = 0;
    stats.numGeometries = 0;
    stats.sourceSize = 0;
    stats.xktSize = 0;
    stats.texturesSize = 0;
    stats.xktVersion = "";
    stats.compressionRatio = 0;
    stats.conversionTime = 0;
    stats.aabb = null;

    function getFileExtension(fileName) {
        let ext = path.extname(fileName);
        if (ext.charAt(0) === ".") {
            ext = ext.substring(1);
        }
        return ext;
    }

    return new Promise(function (resolve, reject) {
        const _log = log;
        log = (msg) => {
            _log(`[convert2xkt] ${msg}`)
        }

        if (!source && !sourceData) {
            reject("Argument expected: source or sourceData");
            return;
        }

        if (!sourceFormat && sourceData) {
            reject("Argument expected: sourceFormat is required with sourceData");
            return;
        }

        if (!output && !outputXKTModel && !outputXKT) {
            reject("Argument expected: output, outputXKTModel or outputXKT");
            return;
        }

        if (source) {
            log('Reading input file: ' + source);
        }

        const startTime = new Date();

        const sourceConfigs = configs.sourceConfigs || {};
        const ext = sourceFormat || getFileExtension(source);

        log(`Input file extension: "${ext}"`);

        let fileTypeConfigs = sourceConfigs[ext];

        if (!fileTypeConfigs) {
            log(`[WARNING] Could not find configs sourceConfigs entry for source format "${ext}". This is derived from the source file name extension. Will use internal default configs.`);
            fileTypeConfigs = {};
        }

        function overrideOption(option1, option2) {
            if (option1 !== undefined) {
                return option1;
            }
            return option2;
        }

        if (!sourceData) {
            try {
                sourceData = fs.readFileSync(source);
            } catch (err) {
                reject(err);
                return;
            }
        }

        const sourceFileSizeBytes = sourceData.byteLength;

        log("Input file size: " + (sourceFileSizeBytes / 1000).toFixed(2) + " kB");

        if (!metaModelDataStr && metaModelSource) {
            log('Reading input metadata file: ' + metaModelSource);
            try {
                metaModelDataStr = fs.readFileSync(metaModelSource);
            } catch (err) {
                reject(err);
                return;
            }
        } else {
            log(`Not embedding metadata in XKT`);
        }

        let metaModelJSON;

        if (metaModelDataStr) {
            try {
                metaModelJSON = JSON.parse(metaModelDataStr);
            } catch (e) {
                metaModelJSON = {};
                log(`Error parsing metadata JSON: ${e}`);
            }
        }

        minTileSize = overrideOption(fileTypeConfigs.minTileSize, minTileSize);
        rotateX = overrideOption(fileTypeConfigs.rotateX, rotateX);
        reuseGeometries = overrideOption(fileTypeConfigs.reuseGeometries, reuseGeometries);
        includeTextures = overrideOption(fileTypeConfigs.includeTextures, includeTextures);
        includeNormals = overrideOption(fileTypeConfigs.includeNormals, includeNormals);
        includeTypes = overrideOption(fileTypeConfigs.includeTypes, includeTypes);
        excludeTypes = overrideOption(fileTypeConfigs.excludeTypes, excludeTypes);

        if (reuseGeometries === false) {
            log("Geometry reuse is disabled");
        }

        const xktModel = new XKTModel({
            minTileSize,
            modelAABB
        });

        switch (ext) {
            case "json":
                convert(parseCityJSONIntoXKTModel, {
                    data: JSON.parse(sourceData),
                    xktModel,
                    stats,
                    rotateX,
                    center: fileTypeConfigs.center,
                    transform: fileTypeConfigs.transform,
                    log
                });
                break;

            case "glb":
                sourceData = toArrayBuffer(sourceData);
                convert(parseGLTFIntoXKTModel, {
                    data: sourceData,
                    reuseGeometries,
                    includeTextures: true,
                    includeNormals,
                    metaModelData: metaModelJSON,
                    xktModel,
                    stats,
                    log
                });
                break;

            case "gltf":
                sourceData = toArrayBuffer(sourceData);
                const gltfBasePath = source ? path.dirname(source) : "";
                convert(parseGLTFIntoXKTModel, {
                    baseUri: gltfBasePath,
                    data: sourceData,
                    reuseGeometries,
                    includeTextures: true,
                    includeNormals,
                    metaModelData: metaModelJSON,
                    xktModel,
                    stats,
                    log
                });
                break;

            // case "gltf":
            //     const gltfJSON = JSON.parse(sourceData);
            //     const gltfBasePath = source ? getBasePath(source) : "";
            //     convert(parseGLTFIntoXKTModel, {
            //         baseUri: gltfBasePath,
            //         data: gltfJSON,
            //         reuseGeometries,
            //         includeTextures,
            //         includeNormals,
            //         metaModelData: metaModelJSON,
            //         xktModel,
            //         getAttachment: async (name) => {
            //             const filePath = gltfBasePath + name;
            //             log(`Reading attachment file: ${filePath}`);
            //             const buffer = fs.readFileSync(filePath);
            //             const arrayBuf = toArrayBuffer(buffer);
            //             return arrayBuf;
            //         },
            //         stats,
            //         log
            //     });
            //     break;

            case "ifc":
                convert(parseIFCIntoXKTModel, {
                    WebIFC,
                    data: sourceData,
                    xktModel,
                    wasmPath: "./",
                    includeTypes,
                    excludeTypes,
                    stats,
                    log
                });
                break;

            case "laz":
                convert(parseLASIntoXKTModel, {
                    data: sourceData,
                    xktModel,
                    stats,
                    fp64: fileTypeConfigs.fp64,
                    colorDepth: fileTypeConfigs.colorDepth,
                    center: fileTypeConfigs.center,
                    transform: fileTypeConfigs.transform,
                    skip: overrideOption(fileTypeConfigs.skip, 1),
                    log
                });
                break;

            case "las":
                convert(parseLASIntoXKTModel, {
                    data: sourceData,
                    xktModel,
                    stats,
                    fp64: fileTypeConfigs.fp64,
                    colorDepth: fileTypeConfigs.colorDepth,
                    center: fileTypeConfigs.center,
                    transform: fileTypeConfigs.transform,
                    skip: overrideOption(fileTypeConfigs.skip, 1),
                    log
                });
                break;

            case "pcd":
                convert(parsePCDIntoXKTModel, {
                    data: sourceData,
                    xktModel,
                    stats,
                    log
                });
                break;

            case "ply":
                convert(parsePLYIntoXKTModel, {
                    data: sourceData,
                    xktModel,
                    stats,
                    log
                });
                break;

            case "stl":
                convert(parseSTLIntoXKTModel, {
                    data: sourceData,
                    xktModel,
                    stats,
                    log
                });
                break;

            default:
                reject(`Error: unsupported source format: "${ext}".`);
                return;
        }

        function convert(parser, converterParams) {

            parser(converterParams).then(() => {

                if (!metaModelJSON) {
                    log("Creating default metamodel in XKT");
                    xktModel.createDefaultMetaObjects();
                }

                log("Input file parsed OK. Building XKT document...");

                xktModel.finalize().then(() => {

                    log("XKT document built OK. Writing to XKT file...");

                    const xktArrayBuffer = writeXKTModelToArrayBuffer(xktModel, metaModelJSON, stats, {zip: zip});

                    const xktContent = Buffer.from(xktArrayBuffer);

                    const targetFileSizeBytes = xktArrayBuffer.byteLength;

                    stats.minTileSize = minTileSize || 200;
                    stats.sourceSize = (sourceFileSizeBytes / 1000).toFixed(2);
                    stats.xktSize = (targetFileSizeBytes / 1000).toFixed(2);
                    stats.xktVersion = zip ? 10 : XKT_INFO.xktVersion;
                    stats.compressionRatio = (sourceFileSizeBytes / targetFileSizeBytes).toFixed(2);
                    stats.conversionTime = ((new Date() - startTime) / 1000.0).toFixed(2);
                    stats.aabb = xktModel.aabb;
                    log(`Converted to: XKT v${stats.xktVersion}`);
                    if (includeTypes) {
                        log("Include types: " + (includeTypes ? includeTypes : "(include all)"));
                    }
                    if (excludeTypes) {
                        log("Exclude types: " + (excludeTypes ? excludeTypes : "(exclude none)"));
                    }
                    log("XKT size: " + stats.xktSize + " kB");
                    log("XKT textures size: " + (stats.texturesSize / 1000).toFixed(2) + "kB");
                    log("Compression ratio: " + stats.compressionRatio);
                    log("Conversion time: " + stats.conversionTime + " s");
                    log("Converted metaobjects: " + stats.numMetaObjects);
                    log("Converted property sets: " + stats.numPropertySets);
                    log("Converted drawable objects: " + stats.numObjects);
                    log("Converted geometries: " + stats.numGeometries);
                    log("Converted textures: " + stats.numTextures);
                    log("Converted textureSets: " + stats.numTextureSets);
                    log("Converted triangles: " + stats.numTriangles);
                    log("Converted vertices: " + stats.numVertices);
                    log("Converted UVs: " + stats.numUVs);
                    log("Converted normals: " + stats.numNormals);
                    log("Converted tiles: " + xktModel.tilesList.length);
                    log("minTileSize: " + stats.minTileSize);

                    if (output) {
                        const outputDir = path.dirname(output);
                        if (outputDir !== "" && !fs.existsSync(outputDir)) {
                            fs.mkdirSync(outputDir, {recursive: true});
                        }
                        log('Writing XKT file: ' + output);
                        fs.writeFileSync(output, xktContent);
                    }

                    if (outputXKTModel) {
                        outputXKTModel(xktModel);
                    }

                    if (outputXKT) {
                        outputXKT(xktContent);
                    }

                    if (outputStats) {
                        outputStats(stats);
                    }

                    resolve();
                });
            }, (err) => {
                reject(err);
            });
        }
    });
}

export {convert2xkt};