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};