Reference Source

src/parsers/parseIFCIntoXKTModel.js

/**
 * @desc Parses IFC STEP file data into an {@link XKTModel}.
 *
 * This function uses [web-ifc](https://github.com/tomvandig/web-ifc) to parse the IFC, which relies on a
 * WASM file to do the parsing.
 *
 * Depending on how we use this function, we may need to provide it with a path to the directory where that WASM file is stored.
 *
 * This function is tested with web-ifc version 0.0.34.
 *
 * ## Usage
 *
 * In the example below we'll create an {@link XKTModel}, then load an IFC model into it.
 *
 * ````javascript
 * import {XKTModel, parseIFCIntoXKTModel, writeXKTModelToArrayBuffer} from "xeokit-convert.es.js";
 *
 * import * as WebIFC from "web-ifc-api.js";
 *
 * utils.loadArraybuffer("rac_advanced_sample_project.ifc", async (data) => {
 *
 *     const xktModel = new XKTModel();
 *
 *     parseIFCIntoXKTModel({
 *          WebIFC,
 *          data,
 *          xktModel,
 *          wasmPath: "../dist/",
 *          autoNormals: true,
 *          log: (msg) => { console.log(msg); }
 *     }).then(()=>{
 *        xktModel.finalize();
 *     },
 *     (msg) => {
 *         console.error(msg);
 *     });
 * });
 * ````
 *
 * @param {Object} params Parsing params.
 * @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 {ArrayBuffer} [params.data] IFC file data.
 * @param {XKTModel} [params.xktModel] XKTModel to parse into.
 * @param {Boolean} [params.autoNormals=true] When true, the parser will ignore the IFC geometry normals, and the IFC
 * data will 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 representation
 * of the IFC model. This is ````true```` by default, because IFC models tend to look acceptable with flat-shading,
 * and we always want to minimize IFC model size wherever possible.
 * @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 {String} params.wasmPath Path to ````web-ifc.wasm````, required by this function.
 * @param {Object} [params.stats={}] Collects statistics.
 * @param {function} [params.log] Logging callback.
 * @returns {Promise} Resolves when IFC has been parsed.
 */
function parseIFCIntoXKTModel({
                                  WebIFC,
                                  data,
                                  xktModel,
                                  autoNormals = true,
                                  includeTypes,
                                  excludeTypes,
                                  wasmPath,
                                  stats = {},
                                  log
                              }) {

    if (log) {
        log("Using parser: parseIFCIntoXKTModel");
    }

    return new Promise(function (resolve, reject) {

        if (!data) {
            reject("Argument expected: data");
            return;
        }

        if (!xktModel) {
            reject("Argument expected: xktModel");
            return;
        }

        if (!wasmPath) {
            reject("Argument expected: wasmPath");
            return;
        }

        const ifcAPI = new WebIFC.IfcAPI();

        if (wasmPath) {
            ifcAPI.SetWasmPath(wasmPath);
        }

        ifcAPI.Init().then(() => {

            const dataArray = new Uint8Array(data);

            const modelID = ifcAPI.OpenModel(dataArray);

            stats.sourceFormat = "IFC";
            stats.schemaVersion = "";
            stats.title = "";
            stats.author = "";
            stats.created = "";
            stats.numMetaObjects = 0;
            stats.numPropertySets = 0;
            stats.numObjects = 0;
            stats.numGeometries = 0;
            stats.numTriangles = 0;
            stats.numVertices = 0;

            const ctx = {
                WebIFC,
                modelID,
                ifcAPI,
                xktModel,
                autoNormals,
                log: (log || function (msg) {
                }),
                nextId: 0,
                stats
            };

            if (includeTypes) {
                ctx.includeTypes = {};
                for (let i = 0, len = includeTypes.length; i < len; i++) {
                    ctx.includeTypes[includeTypes[i]] = true;
                }
            }

            if (excludeTypes) {
                ctx.excludeTypes = {};
                for (let i = 0, len = excludeTypes.length; i < len; i++) {
                    ctx.excludeTypes[excludeTypes[i]] = true;
                }
            }

            const lines = ctx.ifcAPI.GetLineIDsWithType(modelID, WebIFC.IFCPROJECT);
            const ifcProjectId = lines.get(0);
            const ifcProject = ctx.ifcAPI.GetLine(modelID, ifcProjectId);

            ctx.xktModel.schema = "";
            ctx.xktModel.modelId = "" + modelID;
            ctx.xktModel.projectId = "" + ifcProjectId;

            parseMetadata(ctx);
            parseGeometry(ctx);
            parsePropertySets(ctx);

            resolve();

        }).catch((e) => {

            reject(e);
        })
    });
}

function parsePropertySets(ctx) {

    const lines = ctx.ifcAPI.GetLineIDsWithType(ctx.modelID, ctx.WebIFC.IFCRELDEFINESBYPROPERTIES);

    for (let i = 0; i < lines.size(); i++) {

        let relID = lines.get(i);

        let rel = ctx.ifcAPI.GetLine(ctx.modelID, relID, true);

        if (rel) {

            const relatingPropertyDefinition = rel.RelatingPropertyDefinition;
            if (!relatingPropertyDefinition) {
                continue;
            }

            const propertySetId = relatingPropertyDefinition.GlobalId.value;

            const relatedObjects = rel.RelatedObjects;
            if (relatedObjects) {
                for (let i = 0, len = relatedObjects.length; i < len; i++) {
                    const relatedObject = relatedObjects[i];
                    const metaObjectId = relatedObject.GlobalId.value;
                    const metaObject = ctx.xktModel.metaObjects[metaObjectId];
                    if (metaObject) {
                        if (!metaObject.propertySetIds) {
                            metaObject.propertySetIds = [];
                        }
                        metaObject.propertySetIds.push(propertySetId);
                    }
                }
            }

            const props = relatingPropertyDefinition.HasProperties;
            if (props && props.length > 0) {
                const propertySetType = "Default";
                const propertySetName = relatingPropertyDefinition.Name.value;
                const properties = [];
                for (let i = 0, len = props.length; i < len; i++) {
                    const prop = props[i];
                    const name = prop.Name;
                    const nominalValue = prop.NominalValue;
                    if (name && nominalValue) {
                        const property = {
                            name: name.value,
                            type: nominalValue.type,
                            value: nominalValue.value,
                            valueType: nominalValue.valueType
                        };
                        if (prop.Description) {
                            property.description = prop.Description.value;
                        } else if (nominalValue.description) {
                            property.description = nominalValue.description;
                        }
                        properties.push(property);
                    }
                }
                ctx.xktModel.createPropertySet({propertySetId, propertySetType, propertySetName, properties});
                ctx.stats.numPropertySets++;
            }
        }
    }
}

function parseMetadata(ctx) {

    const lines = ctx.ifcAPI.GetLineIDsWithType(ctx.modelID, ctx.WebIFC.IFCPROJECT);
    const ifcProjectId = lines.get(0);
    const ifcProject = ctx.ifcAPI.GetLine(ctx.modelID, ifcProjectId);

    parseSpatialChildren(ctx, ifcProject);
}

function parseSpatialChildren(ctx, ifcElement, parentMetaObjectId) {

    const metaObjectType = ifcElement.__proto__.constructor.name;

    if (ctx.includeTypes && (!ctx.includeTypes[metaObjectType])) {
        return;
    }

    if (ctx.excludeTypes && ctx.excludeTypes[metaObjectType]) {
        return;
    }

    createMetaObject(ctx, ifcElement, parentMetaObjectId);

    const metaObjectId = ifcElement.GlobalId.value;

    parseRelatedItemsOfType(
        ctx,
        ifcElement.expressID,
        'RelatingObject',
        'RelatedObjects',
        ctx.WebIFC.IFCRELAGGREGATES,
        metaObjectId);

    parseRelatedItemsOfType(
        ctx,
        ifcElement.expressID,
        'RelatingStructure',
        'RelatedElements',
        ctx.WebIFC.IFCRELCONTAINEDINSPATIALSTRUCTURE,
        metaObjectId);
}

function createMetaObject(ctx, ifcElement, parentMetaObjectId) {

    const metaObjectId = ifcElement.GlobalId.value;
    const propertySetIds = null;
    const metaObjectType = ifcElement.__proto__.constructor.name;
    const metaObjectName = (ifcElement.Name && ifcElement.Name.value !== "") ? ifcElement.Name.value : metaObjectType;

    ctx.xktModel.createMetaObject({metaObjectId, propertySetIds, metaObjectType, metaObjectName, parentMetaObjectId});
    ctx.stats.numMetaObjects++;
}

function parseRelatedItemsOfType(ctx, id, relation, related, type, parentMetaObjectId) {

    const lines = ctx.ifcAPI.GetLineIDsWithType(ctx.modelID, type);

    for (let i = 0; i < lines.size(); i++) {

        const relID = lines.get(i);
        const rel = ctx.ifcAPI.GetLine(ctx.modelID, relID);
        const relatedItems = rel[relation];

        let foundElement = false;

        if (Array.isArray(relatedItems)) {
            const values = relatedItems.map((item) => item.value);
            foundElement = values.includes(id);

        } else {
            foundElement = (relatedItems.value === id);
        }

        if (foundElement) {

            const element = rel[related];

            if (!Array.isArray(element)) {

                const ifcElement = ctx.ifcAPI.GetLine(ctx.modelID, element.value);

                parseSpatialChildren(ctx, ifcElement, parentMetaObjectId);

            } else {

                element.forEach((element2) => {

                    const ifcElement = ctx.ifcAPI.GetLine(ctx.modelID, element2.value);

                    parseSpatialChildren(ctx, ifcElement, parentMetaObjectId);
                });
            }
        }
    }
}

function parseGeometry(ctx) {

    // Parses the geometry and materials in the IFC, creates
    // XKTEntity, XKTMesh and XKTGeometry components within the XKTModel.

    const flatMeshes = ctx.ifcAPI.LoadAllGeometry(ctx.modelID);

    for (let i = 0, len = flatMeshes.size(); i < len; i++) {
        const flatMesh = flatMeshes.get(i);
        createObject(ctx, flatMesh);
    }

    // LoadAllGeometry does not return IFCSpace meshes
    // here is a workaround

    const lines = ctx.ifcAPI.GetLineIDsWithType(ctx.modelID, ctx.WebIFC.IFCSPACE);
    for (let j = 0, len = lines.size(); j < len; j++) {
        const ifcSpaceId = lines.get(j);
        const flatMesh = ctx.ifcAPI.GetFlatMesh(ctx.modelID, ifcSpaceId);
        createObject(ctx, flatMesh);
    }
}

function createObject(ctx, flatMesh) {

    const flatMeshExpressID = flatMesh.expressID;
    const placedGeometries = flatMesh.geometries;

    const meshIds = [];

    const properties = ctx.ifcAPI.GetLine(ctx.modelID, flatMeshExpressID);
    const entityId = properties.GlobalId.value;

    const metaObjectId = entityId;
    const metaObject = ctx.xktModel.metaObjects[metaObjectId];

    if (ctx.includeTypes && (!metaObject || (!ctx.includeTypes[metaObject.metaObjectType]))) {
        return;
    }

    if (ctx.excludeTypes && (!metaObject || ctx.excludeTypes[metaObject.metaObjectType])) {
        console.log("excluding: " + metaObjectId)
        return;
    }

    for (let j = 0, lenj = placedGeometries.size(); j < lenj; j++) {

        const placedGeometry = placedGeometries.get(j);
        const geometryId = "" + placedGeometry.geometryExpressID;

        if (!ctx.xktModel.geometries[geometryId]) {

            const geometry = ctx.ifcAPI.GetGeometry(ctx.modelID, placedGeometry.geometryExpressID);
            const vertexData = ctx.ifcAPI.GetVertexArray(geometry.GetVertexData(), geometry.GetVertexDataSize());
            const indices = ctx.ifcAPI.GetIndexArray(geometry.GetIndexData(), geometry.GetIndexDataSize());

            // De-interleave vertex arrays

            const positions = [];
            const normals = [];

            for (let k = 0, lenk = vertexData.length / 6; k < lenk; k++) {
                positions.push(vertexData[k * 6 + 0]);
                positions.push(vertexData[k * 6 + 1]);
                positions.push(vertexData[k * 6 + 2]);
            }

            if (!ctx.autoNormals) {
                for (let k = 0, lenk = vertexData.length / 6; k < lenk; k++) {
                    normals.push(vertexData[k * 6 + 3]);
                    normals.push(vertexData[k * 6 + 4]);
                    normals.push(vertexData[k * 6 + 5]);
                }
            }

            ctx.xktModel.createGeometry({
                geometryId: geometryId,
                primitiveType: "triangles",
                positions: positions,
                normals: ctx.autoNormals ? null : normals,
                indices: indices
            });

            ctx.stats.numGeometries++;
            ctx.stats.numVertices += (positions.length / 3);
            ctx.stats.numTriangles += (indices.length / 3);
        }

        const meshId = ("mesh" + ctx.nextId++);

        ctx.xktModel.createMesh({
            meshId: meshId,
            geometryId: geometryId,
            matrix: placedGeometry.flatTransformation,
            color: [placedGeometry.color.x, placedGeometry.color.y, placedGeometry.color.z],
            opacity: placedGeometry.color.w
        });

        meshIds.push(meshId);
    }

    if (meshIds.length > 0) {
        ctx.xktModel.createEntity({
            entityId: entityId,
            meshIds: meshIds
        });
        ctx.stats.numObjects++;
    }
}

export {parseIFCIntoXKTModel};