web-ifc-viewer
Version:
482 lines • 21.2 kB
JavaScript
import { GLTFLoader } from 'three/examples/jsm/loaders/GLTFLoader';
import { BufferAttribute, BufferGeometry, Mesh, MeshLambertMaterial } from 'three';
import { GLTFExporter } from 'three/examples/jsm/exporters/GLTFExporter';
import { IFCLoader } from 'web-ifc-three/IFCLoader';
import { IFCPROJECT } from 'web-ifc';
import { IfcComponent } from '../../base-types';
import { disposeMeshRecursively } from '../../utils/ThreeUtils';
import { StoreyManager } from '../display/plans/storey-manager';
export class GLTFManager extends IfcComponent {
constructor(context, IFC) {
super(context);
this.context = context;
this.IFC = IFC;
this.GLTFModels = {};
this.loader = new GLTFLoader();
this.exporter = new GLTFExporter();
this.tempIfcLoader = null;
this.allFloors = 'allFloors';
this.allCategories = 'allCategories';
this.stories = new StoreyManager();
this.options = {
trs: false,
onlyVisible: false,
truncateDrawRange: true,
binary: true,
maxTextureSize: 0
};
this.setupGeometryIndexDraco = (meshes, geometry) => {
let off = 0;
const offsets = [];
for (let i = 0; i < meshes.length; i++) {
offsets.push(off);
// eslint-disable-next-line no-underscore-dangle
off += meshes[i].geometry.attributes._expressid.count;
}
const indices = meshes.map((mesh, i) => {
const index = mesh.geometry.index;
return !index ? [] : new Uint32Array(index.array).map((value) => value + offsets[i]);
});
geometry.setIndex(this.flattenIndices(indices));
};
this.flattenIndices = (indices) => {
const indexArray = [];
for (let i = 0; i < indices.length; i++) {
for (let j = 0; j < indices[i].length; j++) {
indexArray.push(indices[i][j]);
}
}
return indexArray;
};
}
dispose() {
this.loader = null;
this.exporter = null;
const models = Object.values(this.GLTFModels);
models.forEach((model) => {
model.removeFromParent();
model.children.forEach((child) => disposeMeshRecursively(child));
});
this.GLTFModels = null;
this.stories.dispose();
this.stories = null;
}
/**
* Loads any glTF file into the scene using [Three.js loader](https://threejs.org/docs/#examples/en/loaders/GLTFLoader).
* @url The URL of the GLTF file to load
* @onProgress (optional) A callback function that is called when the file is loaded
*/
async load(url, onProgress) {
const loaded = (await this.loader.loadAsync(url, onProgress));
const mesh = loaded.scene;
const modelID = this.getModelID();
this.GLTFModels[modelID] = mesh;
this.context.getScene().add(mesh);
return mesh;
}
/**
* Load glTF and enable IFC.js tools over it.
* This just works if the glTF was previously exported from an IFC model using `exportIfcAsGltf()`.
* @url The URL of the GLTF file to load
*/
async loadModel(url) {
const gltfMesh = await this.getGltfMesh(url);
gltfMesh.geometry.computeBoundsTree();
gltfMesh.modelID = this.getModelID();
this.context.getScene().add(gltfMesh);
this.setupMeshAsModel(gltfMesh);
return gltfMesh;
}
// TODO: Split up in smaller methods AND bring to new file
/**
* Exports the specified IFC file (or file subset) as glTF.
* @fileURL The URL of the IFC file to convert to glTF
* @ids (optional) The ids of the items to export. If not defined, the full model is exported
*/
async exportIfcFileAsGltf(config) {
const { ifcFileUrl, getProperties, categories, splitByFloors, maxJSONSize, onProgress, coordinationMatrix } = config;
const { loader, manager } = await this.setupIfcLoader(coordinationMatrix);
const model = await loader.loadAsync(ifcFileUrl, (event) => {
if (onProgress)
onProgress(event.loaded, event.total, 'IFC');
});
const result = {
gltf: {},
json: [],
id: '',
coordinationMatrix: []
};
const projects = await manager.getAllItemsOfType(model.modelID, IFCPROJECT, true);
if (!projects.length)
throw new Error('No IfcProject instances were found in the IFC.');
const GUID = projects[0].GlobalId;
if (!GUID)
throw new Error('The found IfcProject does not have a GUID');
result.id = GUID.value;
result.coordinationMatrix = await manager.ifcAPI.GetCoordinationMatrix(0);
let allIdsByFloor = {};
let floorNames = [];
if (splitByFloors) {
allIdsByFloor = await this.getIDsByFloor(loader);
floorNames = Object.keys(allIdsByFloor);
}
await this.getModels(categories, result, manager, splitByFloors, floorNames, allIdsByFloor, model, onProgress);
if (getProperties) {
await this.getProperties(model, maxJSONSize, onProgress, result);
}
await loader.ifcManager.dispose();
this.tempIfcLoader = null;
return result;
}
async getProperties(model, maxJSONSize, onProgress, result) {
const previousLoader = this.IFC.properties.loader;
this.IFC.properties.loader = this.tempIfcLoader;
const jsons = await this.IFC.properties.serializeAllProperties(model, maxJSONSize, (progress, total) => {
if (onProgress)
onProgress(progress, total, 'JSON');
});
result.json = jsons.map((json) => new File([json], 'properties.json'));
this.IFC.properties.loader = previousLoader;
}
async getModels(categories, result, manager, splitByFloors, floorNames, allIdsByFloor, model, onProgress) {
if (categories) {
await this.getModelsByCategory(categories, result, manager, splitByFloors, floorNames, allIdsByFloor, model, onProgress);
}
else {
await this.getModelsWithoutCategories(result, splitByFloors, floorNames, allIdsByFloor, model);
}
}
async setupIfcLoader(coordinationMatrix) {
const loader = new IFCLoader();
this.tempIfcLoader = loader;
const state = this.IFC.loader.ifcManager.state;
const manager = loader.ifcManager;
if (state.wasmPath)
await manager.setWasmPath(state.wasmPath);
if (state.worker.active)
await manager.useWebWorkers(true, state.worker.path);
if (state.webIfcSettings)
await manager.applyWebIfcConfig(state.webIfcSettings);
await manager.parser.setupOptionalCategories(this.IFC.loader.ifcManager.parser.optionalCategories);
if (coordinationMatrix) {
await this.overrideCoordMatrix(manager, coordinationMatrix);
}
return { loader, manager };
}
async overrideCoordMatrix(manager, coordinationMatrix) {
manager.setupCoordinationMatrix(coordinationMatrix);
}
async getModelsByCategory(categories, result, manager, splitByFloors, floorNames, allIdsByFloor, model, onProgress) {
var _a;
const items = [];
const categoryNames = Object.keys(categories);
for (let i = 0; i < categoryNames.length; i++) {
const categoryName = categoryNames[i];
const currentCategories = categories[categoryName];
if (!result.gltf[categoryName])
result.gltf[categoryName] = {};
for (let j = 0; j < currentCategories.length; j++) {
const foundItems = await manager.getAllItemsOfType(0, currentCategories[j], false);
items.push(...foundItems);
}
const groupedIDs = {};
if (splitByFloors) {
floorNames.forEach((floorName) => {
const floorIDs = allIdsByFloor[floorName];
groupedIDs[floorName] = items.filter((id) => floorIDs.ids.has(id));
});
}
else {
groupedIDs[this.allFloors] = items;
}
const foundFloorNames = Object.keys(groupedIDs);
for (let j = 0; j < foundFloorNames.length; j++) {
const foundFloorName = foundFloorNames[j];
const items = groupedIDs[foundFloorName];
if (items.length) {
const gltf = await this.exportModelPartToGltf(model, items, true);
const height = ((_a = allIdsByFloor[foundFloorName]) === null || _a === void 0 ? void 0 : _a.height) || 0;
result.gltf[categoryName][foundFloorName] = {
file: this.glTFToFile(gltf, 'model-part.gltf'),
height
};
}
else {
result.gltf[categoryName][foundFloorName] = {
file: null,
height: 0
};
}
}
if (onProgress)
onProgress(i, categoryNames === null || categoryNames === void 0 ? void 0 : categoryNames.length, 'GLTF');
items.length = 0;
}
}
async getModelsWithoutCategories(result, splitByFloors, floorNames, allIdsByFloor, model) {
result.gltf[this.allCategories] = {};
if (splitByFloors) {
for (let i = 0; i < floorNames.length; i++) {
const floorName = floorNames[i];
const floorIDs = Array.from(allIdsByFloor[floorName].ids);
const gltf = await this.exportModelPartToGltf(model, floorIDs, true);
result.gltf[this.allCategories][floorName] = {
file: this.glTFToFile(gltf),
height: allIdsByFloor[floorName].height
};
}
}
else {
const gltf = await this.exportMeshToGltf(model);
result.gltf[this.allCategories][this.allFloors] = {
file: this.glTFToFile(gltf),
height: 0
};
}
}
// TODO: Is this really necessary? Maybe with exporting the file is enough
/**
* Exports the specified model (or model subset) as glTF.
* @modelID The ID of the IFC model to convert to glTF
* @ids (optional) The ids of the items to export. If not defined, the full model is exported
*/
async exportIfcAsGltf(modelID, ids) {
const model = this.context.items.ifcModels.find((model) => model.modelID === modelID);
if (!model)
throw new Error('The specified model does not exist!');
return ids ? this.exportModelPartToGltf(model, ids) : this.exportMeshToGltf(model);
}
/**
* Exports the given mesh as glTF.
* @mesh The mesh to export.
*/
exportMeshToGltf(mesh) {
return new Promise((resolve) => {
this.exporter.parse(mesh, (result) => resolve(result), this.options);
});
}
// TODO: Split up in smaller methods AND bring to new file
exportModelPartToGltf(model, ids, useTempLoader = false) {
const coordinates = [];
const expressIDs = [];
const newIndices = [];
const alreadySaved = new Map();
const customID = 'temp-gltf-subset';
const loader = useTempLoader ? this.tempIfcLoader : this.IFC.loader;
if (!loader)
throw new Error('IFCLoader could not be found!');
const subset = loader.ifcManager.createSubset({
modelID: model.modelID,
ids,
removePrevious: true,
customID
});
if (!subset.geometry.index)
throw new Error('Geometry must be indexed!');
const positionAttr = subset.geometry.attributes.position;
const expressIDAttr = subset.geometry.attributes.expressID;
const newGroups = subset.geometry.groups.filter((group) => group.count !== 0);
const newMaterials = [];
const prevMaterials = subset.material;
let newMaterialIndex = 0;
newGroups.forEach((group) => {
newMaterials.push(prevMaterials[group.materialIndex]);
group.materialIndex = newMaterialIndex++;
});
let newIndex = 0;
for (let i = 0; i < subset.geometry.index.count; i++) {
const index = subset.geometry.index.array[i];
if (!alreadySaved.has(index)) {
coordinates.push(positionAttr.array[3 * index]);
coordinates.push(positionAttr.array[3 * index + 1]);
coordinates.push(positionAttr.array[3 * index + 2]);
expressIDs.push(expressIDAttr.getX(index));
alreadySaved.set(index, newIndex++);
}
const saved = alreadySaved.get(index);
newIndices.push(saved);
}
const geometryToExport = new BufferGeometry();
const newVerticesAttr = new BufferAttribute(Float32Array.from(coordinates), 3);
const newExpressIDAttr = new BufferAttribute(Uint32Array.from(expressIDs), 1);
geometryToExport.setAttribute('position', newVerticesAttr);
geometryToExport.setAttribute('expressID', newExpressIDAttr);
geometryToExport.setIndex(newIndices);
geometryToExport.groups = newGroups;
geometryToExport.computeVertexNormals();
loader.ifcManager.removeSubset(model.modelID, undefined, customID);
const mesh = new Mesh(geometryToExport, newMaterials);
return this.exportMeshToGltf(mesh);
}
glTFToFile(gltf, name = 'model.gltf') {
return new File([new Blob([gltf])], name);
}
async getIDsByFloor(loader) {
const ifcProject = await loader.ifcManager.getSpatialStructure(0);
const idsByFloor = {};
const storeys = ifcProject.children[0].children[0].children;
const storeysIDs = storeys.map((storey) => storey.expressID);
this.stories.loader = loader;
const heightsByName = await this.stories.getAbsoluteElevation(0);
const heights = Object.values(heightsByName);
for (let i = 0; i < storeysIDs.length; i++) {
const storey = storeys[i];
const ids = [];
this.getChildrenRecursively(storey, ids);
const storeyID = storeysIDs[i];
const properties = await loader.ifcManager.getItemProperties(0, storeyID);
const name = this.getStoreyName(properties);
const height = heights[i];
idsByFloor[name] = {
ids: new Set(ids),
height
};
}
return idsByFloor;
}
getStoreyName(storey) {
if (storey.Name)
return storey.Name.value;
if (storey.LongName)
return storey.LongName.value;
return storey.GlobalId;
}
getChildrenRecursively(spatialNode, result) {
const ids = spatialNode.children.map((child) => child.expressID);
result.push(...ids);
spatialNode.children.forEach((child) => {
if (child.children.length) {
this.getChildrenRecursively(child, result);
}
});
}
getModelID() {
const models = this.context.items.ifcModels;
if (!models.length)
return 0;
const allIDs = models.map((model) => model.modelID);
return Math.max(...allIDs) + 1;
}
async getGltfMesh(url) {
const allMeshes = await this.getMeshes(url);
const geometry = this.getGeometry(allMeshes);
const materials = this.getMaterials(allMeshes);
this.cleanUpLoadedInformation(allMeshes);
return new Mesh(geometry, materials);
}
// Necessary to make the glTF work as a model
setupMeshAsModel(newMesh) {
// TODO: In the future we might want to rethink this or at least fix the typings
this.IFC.loader.ifcManager.state.models[newMesh.modelID] = { mesh: newMesh };
const items = this.context.items;
items.ifcModels.push(newMesh);
items.pickableIfcModels.push(newMesh);
}
cleanUpLoadedInformation(allMeshes) {
allMeshes.forEach((mesh) => {
mesh.geometry.attributes = {};
mesh.geometry.dispose();
mesh.material.dispose();
});
}
getMaterials(allMeshes) {
const clippingPlanes = this.context.getClippingPlanes();
return allMeshes.map((mesh) => {
const material = mesh.material;
return new MeshLambertMaterial({
color: material.color,
transparent: material.opacity !== 1,
opacity: material.opacity,
side: 2,
clippingPlanes
});
});
}
async getMeshes(url) {
const result = await this.load(url);
result.removeFromParent();
const isNested = result.children[0].children.length !== 0;
const meshes = isNested ? result.children[0].children : [result.children[0]];
return meshes;
}
getGeometry(meshes) {
// eslint-disable-next-line no-underscore-dangle
const parseDraco = meshes.length <= 1
? false
: meshes[0].geometry.attributes.position.array !==
meshes[1].geometry.attributes.position.array;
const geometry = new BufferGeometry();
if (parseDraco) {
this.setupGeometryAttributesDraco(geometry, meshes);
this.setupGeometryIndexDraco(meshes, geometry);
}
else {
this.setupGeometryAttributes(geometry, meshes);
this.setupGeometryIndex(meshes, geometry);
}
this.setupGroups(meshes, geometry);
return geometry;
}
setupGeometryAttributes(geometry, meshes) {
// eslint-disable-next-line no-underscore-dangle
geometry.setAttribute('expressID', meshes[0].geometry.attributes._expressid);
geometry.setAttribute('position', meshes[0].geometry.attributes.position);
geometry.setAttribute('normal', meshes[0].geometry.attributes.normal);
}
setupGeometryAttributesDraco(geometry, meshes) {
let intArraryLength = 0;
let floatArrayLength = 0;
for (let i = 0; i < meshes.length; i++) {
const mesh = meshes[i];
const attributes = mesh.geometry.attributes;
// eslint-disable-next-line no-underscore-dangle
intArraryLength += attributes._expressid.array.length;
floatArrayLength += attributes.position.array.length;
}
const expressidArray = new Uint32Array(intArraryLength);
const positionArray = new Float32Array(floatArrayLength);
const normalArray = new Float32Array(floatArrayLength);
this.fillArray(meshes, '_expressid', expressidArray);
this.fillArray(meshes, 'position', positionArray);
this.fillArray(meshes, 'normal', normalArray);
geometry.setAttribute('expressID', new BufferAttribute(expressidArray, 1));
geometry.setAttribute('position', new BufferAttribute(positionArray, 3));
geometry.setAttribute('normal', new BufferAttribute(normalArray, 3));
}
fillArray(meshes, key, arr) {
let offset = 0;
for (let i = 0; i < meshes.length; i++) {
const mesh = meshes[i];
arr.set(mesh.geometry.attributes[key].array, offset);
offset += mesh.geometry.attributes[key].array.length;
}
}
setupGeometryIndex(meshes, geometry) {
const indices = meshes.map((mesh) => {
const index = mesh.geometry.index;
return index ? index.array : [];
});
const indexArray = [];
for (let i = 0; i < indices.length; i++) {
for (let j = 0; j < indices[i].length; j++) {
indexArray.push(indices[i][j]);
}
}
geometry.setIndex(indexArray);
}
setupGroups(meshes, geometry) {
const groupLengths = meshes.map((mesh) => {
const index = mesh.geometry.index;
return index ? index.count : 0;
});
let start = 0;
let materialIndex = 0;
geometry.groups = groupLengths.map((count) => {
const result = { start, count, materialIndex };
materialIndex++;
start += count;
return result;
});
}
}
//# sourceMappingURL=glTF.js.map