diff --git a/Sources/IO/Geometry/IFCImporter/example/index.js b/Sources/IO/Geometry/IFCImporter/example/index.js new file mode 100644 index 00000000000..d9ea0ef5493 --- /dev/null +++ b/Sources/IO/Geometry/IFCImporter/example/index.js @@ -0,0 +1,49 @@ +import '@kitware/vtk.js/favicon'; + +// Load the rendering pieces we want to use (for both WebGL and WebGPU) +import '@kitware/vtk.js/Rendering/Profiles/Geometry'; + +import vtkFullScreenRenderWindow from '@kitware/vtk.js/Rendering/Misc/FullScreenRenderWindow'; +import vtkResourceLoader from '@kitware/vtk.js/IO/Core/ResourceLoader'; +import vtkIFCImporter from '@kitware/vtk.js/IO/Geometry/IFCImporter'; + +// ---------------------------------------------------------------------------- +// Example code +// ---------------------------------------------------------------------------- + +const importer = vtkIFCImporter.newInstance({ + mergeGeometries: true, +}); + +// ---------------------------------------------------------------------------- +function update() { + const fullScreenRenderer = vtkFullScreenRenderWindow.newInstance(); + const renderer = fullScreenRenderer.getRenderer(); + const renderWindow = fullScreenRenderer.getRenderWindow(); + + const resetCamera = renderer.resetCamera; + const render = renderWindow.render; + + importer.onReady(() => { + importer.importActors(renderer); + resetCamera(); + render(); + }); +} + +// ---------------------------------------------------------------------------- +// Importer usage example +// ---------------------------------------------------------------------------- +vtkResourceLoader + .loadScript('https://cdn.jsdelivr.net/npm/web-ifc@0.0.61/web-ifc-api-iife.js') + .then(() => { + // Pass WebIFC api to vtkIFCImporter + vtkIFCImporter.setIFCAPI(window.WebIFC); + + // Trigger data download + importer + .setUrl( + 'https://raw.githubusercontent.com/ThatOpen/engine_web-ifc/refs/heads/main/tests/ifcfiles/public/duplex.ifc' + ) + .then(update); + }); diff --git a/Sources/IO/Geometry/IFCImporter/index.d.ts b/Sources/IO/Geometry/IFCImporter/index.d.ts new file mode 100644 index 00000000000..b43841527c0 --- /dev/null +++ b/Sources/IO/Geometry/IFCImporter/index.d.ts @@ -0,0 +1,163 @@ +import { vtkAlgorithm, vtkObject } from '../../../interfaces'; +import vtkRenderer from '../../../Rendering/Core/Renderer'; +import HtmlDataAccessHelper from '../../Core/DataAccessHelper/HtmlDataAccessHelper'; +import HttpDataAccessHelper from '../../Core/DataAccessHelper/HttpDataAccessHelper'; +import JSZipDataAccessHelper from '../../Core/DataAccessHelper/JSZipDataAccessHelper'; +import LiteHttpDataAccessHelper from '../../Core/DataAccessHelper/LiteHttpDataAccessHelper'; + +interface IIFCImporterOptions { + compression?: string; + progressCallback?: any; +} + +/** + * + */ +export interface IIFCImporterInitialValues { + mergeGeometries?: boolean; +} + +type vtkIFCImporterBase = vtkObject & + Omit< + vtkAlgorithm, + | 'getInputData' + | 'setInputData' + | 'setInputConnection' + | 'getInputConnection' + | 'addInputConnection' + | 'addInputData' + >; + +export interface vtkIFCImporter extends vtkIFCImporterBase { + /** + * + */ + getBaseURL(): string; + + /** + * + */ + getDataAccessHelper(): + | HtmlDataAccessHelper + | HttpDataAccessHelper + | JSZipDataAccessHelper + | LiteHttpDataAccessHelper; + + /** + * Get the url of the object to load. + */ + getUrl(): string; + + /** + * Import actors into the renderer. + * @param {vtkRenderer} renderer The vtkRenderer to import the actors into. + */ + importActors(renderer: vtkRenderer): void; + + /** + * Load the object data. + * @param {IIFCImporterOptions} [options] + */ + loadData(options?: IIFCImporterOptions): Promise; + + /** + * Parse data. + * @param {String | ArrayBuffer} content The content to parse. + */ + parse(content: string | ArrayBuffer): void; + + /** + * Parse data as ArrayBuffer. + * @param {ArrayBuffer} content The content to parse. + */ + parseAsArrayBuffer(content: ArrayBuffer): void; + + /** + * + * @param inData + * @param outData + */ + requestData(inData: any, outData: any): void; + + /** + * + * @param dataAccessHelper + */ + setDataAccessHelper( + dataAccessHelper: + | HtmlDataAccessHelper + | HttpDataAccessHelper + | JSZipDataAccessHelper + | LiteHttpDataAccessHelper + ): boolean; + + /** + * Set the url of the object to load. + * @param {String} url the url of the object to load. + * @param {IIFCImporterOptions} [option] The PLY reader options. + */ + setUrl(url: string, option?: IIFCImporterOptions): Promise; +} + +/** + * Set WebIFC api to be used by vtkIFCImporter + * @param {object} ifcApi + */ +export function setIFCAPI(ifcApi: any): void; + +/** + * Method used to decorate a given object (publicAPI+model) with vtkIFCImporter characteristics. + * + * @param publicAPI object on which methods will be bounds (public) + * @param model object on which data structure will be bounds (protected) + * @param {IIFCImporterInitialValues} [initialValues] (default: {}) + */ +export function extend( + publicAPI: object, + model: object, + initialValues?: IIFCImporterInitialValues +): void; + +/** + * Method used to create a new instance of vtkIFCImporter + * @param {IIFCImporterInitialValues} [initialValues] for pre-setting some of its content + */ +export function newInstance( + initialValues?: IIFCImporterInitialValues +): vtkIFCImporter; + +/** + * vtkIFCImporter is a source object that reads Industry Foundation Class(IFC) files. + * + * The vtkIFCImporter is using web-ifc library to parse the IFC file. + * + * @example + * ```js + * import vtkResourceLoader from '@kitware/vtk.js/IO/Core/ResourceLoader'; + * import vtkIFCImporter from '@kitware/vtk.js/IO/Geometry/IFCImporter'; + * + * function update() { + * importer.onReady(() => { + * importer.importActors(renderer); + * renderer.resetCamera(); + * renderWindow.render(); + * }); + * } + * + * vtkResourceLoader + * .loadScript('https://cdn.jsdelivr.net/npm/web-ifc@0.0.55/web-ifc-api-iife.js') + * .then(() => { + * // Pass WebIFC api to vtkIFCImporter + * vtkIFCImporter.setIFCAPI(window.WebIFC); + * + * // Trigger data download + * importer.setUrl(`${__BASE_PATH__}/data/ifc/house.ifc`).then(update); + * }); + * ``` + */ +export declare const vtkIFCImporter: { + newInstance: typeof newInstance; + extend: typeof extend; + setIFCAPI: typeof setIFCAPI; +}; +export default vtkIFCImporter; diff --git a/Sources/IO/Geometry/IFCImporter/index.js b/Sources/IO/Geometry/IFCImporter/index.js new file mode 100644 index 00000000000..b4969c198f0 --- /dev/null +++ b/Sources/IO/Geometry/IFCImporter/index.js @@ -0,0 +1,321 @@ +import macro from 'vtk.js/Sources/macros'; + +// Enable data soure for DataAccessHelper +import 'vtk.js/Sources/IO/Core/DataAccessHelper/LiteHttpDataAccessHelper'; // Just need HTTP +// import 'vtk.js/Sources/IO/Core/DataAccessHelper/HttpDataAccessHelper'; // HTTP + zip +// import 'vtk.js/Sources/IO/Core/DataAccessHelper/HtmlDataAccessHelper'; // html + base64 + zip +// import 'vtk.js/Sources/IO/Core/DataAccessHelper/JSZipDataAccessHelper'; // zip + +import DataAccessHelper from 'vtk.js/Sources/IO/Core/DataAccessHelper'; +import vtkActor from 'vtk.js/Sources/Rendering/Core/Actor'; +import vtkMapper from 'vtk.js/Sources/Rendering/Core/Mapper'; +import vtkDataArray from 'vtk.js/Sources/Common/Core/DataArray'; +import vtkPolyData from 'vtk.js/Sources/Common/DataModel/PolyData'; +import vtkCellArray from 'vtk.js/Sources/Common/Core/CellArray'; +import vtkAppendPolyData from 'vtk.js/Sources/Filters/General/AppendPolyData'; +import vtkMatrixBuilder from 'vtk.js/Sources/Common/Core/MatrixBuilder'; + +const { vtkErrorMacro } = macro; + +let WebIFC; + +/** + * Set WebIFC api to be used by vtkIFCImporter + * @param {object} ifcApi + */ +function setIFCAPI(ifcApi) { + WebIFC = ifcApi; +} + +function vtkIFCImporter(publicAPI, model) { + model.classHierarchy.push('vtkIFCImporter'); + const meshes = []; + + /** + * Create a vtkPolyData from an IFC mesh object + * @param {object} mesh the IFC web mesh object + * @returns vtkPolyData + */ + function createPolyDataFromIFCMesh(mesh) { + const { vertices, indices } = mesh; + const pd = vtkPolyData.newInstance(); + const cells = vtkCellArray.newInstance(); + + const pointValues = new Float32Array(vertices.length / 2); + const normalsArray = new Float32Array(vertices.length / 2); + + for (let i = 0; i < vertices.length; i += 6) { + pointValues[i / 2] = vertices[i]; + pointValues[i / 2 + 1] = vertices[i + 1]; + pointValues[i / 2 + 2] = vertices[i + 2]; + + normalsArray[i / 2] = vertices[i + 3]; + normalsArray[i / 2 + 1] = vertices[i + 4]; + normalsArray[i / 2 + 2] = vertices[i + 5]; + } + + const nCells = indices.length; + cells.resize((3 * nCells) / 3); + for (let cellId = 0; cellId < nCells; cellId += 3) { + const cell = indices.slice(cellId, cellId + 3); + cells.insertNextCell(cell); + } + + pd.getPoints().setData(pointValues, 3); + pd.setStrips(cells); + pd.getPointData().setNormals( + vtkDataArray.newInstance({ + name: 'Normals', + values: normalsArray, + numberOfComponents: 3, + }) + ); + + return pd; + } + + /** + * Create a colored vtkPolyData from an IFC mesh object + * @param {object} mesh the IFC mesh object + * @returns vtkPolyData + */ + function createColoredPolyDataFromIFCMesh(mesh) { + const { vertices, indices, color, userMatrix } = mesh; + + const pd = vtkPolyData.newInstance(); + const cells = vtkCellArray.newInstance(); + + const pointValues = new Float32Array(vertices.length / 2); + const normalsArray = new Float32Array(vertices.length / 2); + const colorArray = new Float32Array(vertices.length / 2); + + if (userMatrix) { + const transformMatrix = vtkMatrixBuilder + .buildFromRadian() + .setMatrix(userMatrix); + + for (let i = 0; i < vertices.length; i += 6) { + const point = [vertices[i], vertices[i + 1], vertices[i + 2]]; + const normal = [vertices[i + 3], vertices[i + 4], vertices[i + 5]]; + + transformMatrix.apply(point).apply(normal); + + pointValues[i / 2] = point[0]; + pointValues[i / 2 + 1] = point[1]; + pointValues[i / 2 + 2] = point[2]; + + normalsArray[i / 2] = normal[0]; + normalsArray[i / 2 + 1] = normal[1]; + normalsArray[i / 2 + 2] = normal[2]; + + const colorIndex = i / 2; + colorArray[colorIndex] = color.x; + colorArray[colorIndex + 1] = color.y; + colorArray[colorIndex + 2] = color.z; + } + } else { + for (let i = 0; i < vertices.length; i += 6) { + pointValues[i / 2] = vertices[i]; + pointValues[i / 2 + 1] = vertices[i + 1]; + pointValues[i / 2 + 2] = vertices[i + 2]; + + normalsArray[i / 2] = vertices[i + 3]; + normalsArray[i / 2 + 1] = vertices[i + 4]; + normalsArray[i / 2 + 2] = vertices[i + 5]; + + const colorIndex = i / 2; + colorArray[colorIndex] = color.x; + colorArray[colorIndex + 1] = color.y; + colorArray[colorIndex + 2] = color.z; + } + } + + const nCells = indices.length; + cells.resize((3 * nCells) / 3); + for (let cellId = 0; cellId < nCells; cellId += 3) { + const cell = indices.slice(cellId, cellId + 3); + cells.insertNextCell(cell); + } + + pd.getPoints().setData(pointValues, 3); + pd.setPolys(cells); + pd.getPointData().setNormals( + vtkDataArray.newInstance({ + name: 'Normals', + values: normalsArray, + numberOfComponents: 3, + }) + ); + + pd.getPointData().setScalars( + vtkDataArray.newInstance({ + name: 'Colors', + values: colorArray, + numberOfComponents: 3, + }) + ); + + return pd; + } + + function parseIfc(content) { + const modelID = model._ifcApi.OpenModel(new Uint8Array(content), { + COORDINATE_TO_ORIGIN: true, + USE_FAST_BOOLS: true, + }); + + model._ifcApi.StreamAllMeshes(modelID, (mesh) => { + const placedGeometries = mesh.geometries; + + for (let i = 0; i < placedGeometries.size(); i++) { + const placedGeometry = placedGeometries.get(i); + + const ifcGeometryData = model._ifcApi.GetGeometry( + modelID, + placedGeometry.geometryExpressID + ); + + const ifcVertices = model._ifcApi.GetVertexArray( + ifcGeometryData.GetVertexData(), + ifcGeometryData.GetVertexDataSize() + ); + + const ifcIndices = model._ifcApi.GetIndexArray( + ifcGeometryData.GetIndexData(), + ifcGeometryData.GetIndexDataSize() + ); + + meshes.push({ + vertices: ifcVertices, + indices: ifcIndices, + color: placedGeometry.color, + userMatrix: placedGeometry.flatTransformation, + }); + } + }); + + model._ifcApi.CloseModel(modelID); + } + + if (!model.dataAccessHelper) { + model.dataAccessHelper = DataAccessHelper.get('http'); + } + + function fetchData(url, options = {}) { + const { compression, progressCallback } = model; + return model.dataAccessHelper.fetchBinary(url, { + compression, + progressCallback, + }); + } + + publicAPI.setUrl = (url, options = { binary: true }) => { + model.url = url; + model.baseURL = url.split('/').slice(0, -1).join('/'); + model.compression = options.compression; + return publicAPI.loadData(options); + }; + + publicAPI.loadData = (options = {}) => + fetchData(model.url, options).then(publicAPI.parse); + + publicAPI.parse = (content) => { + publicAPI.parseAsArrayBuffer(content); + }; + + publicAPI.parseAsArrayBuffer = (content) => { + if (!content) { + vtkErrorMacro('No content to parse.'); + return; + } + + if (!WebIFC) { + vtkErrorMacro('vtkIFCImporter requires WebIFC API to be set.'); + return; + } + + model._ifcApi = new WebIFC.IfcAPI(); + model._ifcApi.Init().then(() => { + parseIfc(content); + publicAPI.invokeReady(); + }); + }; + + publicAPI.importActors = (renderer) => { + if (model.mergeGeometries) { + const opaqueMeshes = meshes.filter((mesh) => mesh.color.w === 1); + let apd = vtkAppendPolyData.newInstance(); + + opaqueMeshes.forEach((mesh) => { + const pd = createColoredPolyDataFromIFCMesh(mesh); + apd.addInputData(pd); + }); + + let mapper = vtkMapper.newInstance(); + mapper.setColorModeToDirectScalars(); + mapper.setInputConnection(apd.getOutputPort()); + + let actor = vtkActor.newInstance(); + actor.setMapper(mapper); + renderer.addActor(actor); + + const transparentMeshes = meshes.filter((mesh) => mesh.color.w < 1); + apd = vtkAppendPolyData.newInstance(); + + transparentMeshes.forEach((mesh) => { + const pd = createColoredPolyDataFromIFCMesh(mesh); + apd.addInputData(pd); + }); + + mapper = vtkMapper.newInstance(); + mapper.setColorModeToDirectScalars(); + mapper.setInputConnection(apd.getOutputPort()); + + actor = vtkActor.newInstance(); + actor.setMapper(mapper); + actor.getProperty().setOpacity(0.5); + renderer.addActor(actor); + } else { + meshes.forEach((mesh) => { + const pd = createPolyDataFromIFCMesh(mesh); + + const mapper = vtkMapper.newInstance(); + mapper.setInputData(pd); + + const actor = vtkActor.newInstance(); + actor.setMapper(mapper); + + const { x, y, z, w } = mesh.color; + actor.getProperty().setColor(x, y, z); + actor.getProperty().setOpacity(w); + + actor.setUserMatrix(mesh.userMatrix); + renderer.addActor(actor); + }); + } + }; +} + +const DEFAULT_VALUES = { + mergeGeometries: false, +}; + +export function extend(publicAPI, model, initialValues = {}) { + Object.assign(model, DEFAULT_VALUES, initialValues); + + macro.obj(publicAPI, model); + macro.get(publicAPI, model, ['url', 'baseURL']); + macro.setGet(publicAPI, model, ['dataAccessHelper', 'mergeGeometries']); + macro.event(publicAPI, model, 'ready'); + macro.algo(publicAPI, model, 0, 1); + + vtkIFCImporter(publicAPI, model); +} + +export const newInstance = macro.newInstance(extend, 'vtkIFCImporter'); + +export default { + newInstance, + extend, + setIFCAPI, +}; diff --git a/Sources/IO/Geometry/index.js b/Sources/IO/Geometry/index.js index d83543eb7ed..8f31be614be 100644 --- a/Sources/IO/Geometry/index.js +++ b/Sources/IO/Geometry/index.js @@ -4,6 +4,7 @@ import vtkDracoReader from './DracoReader'; import vtkSTLWriter from './STLWriter'; import vtkPLYWriter from './PLYWriter'; import vtkGLTFImporter from './GLTFImporter'; +import vtkIFCImporter from './IFCImporter'; export default { vtkSTLReader, @@ -12,4 +13,5 @@ export default { vtkSTLWriter, vtkPLYWriter, vtkGLTFImporter, + vtkIFCImporter, };