From bf66da21f685f91c22e1301c16b2bc6cca688796 Mon Sep 17 00:00:00 2001 From: TeapoyY Date: Mon, 6 Apr 2026 08:24:02 +0800 Subject: [PATCH 1/2] fix: detect .json files in IFCLoader.load() and provide helpful error message Previously, when a user tried to load a JSON file (produced by the ifc-to-json tool) using IFCLoader.load(), the loader would attempt to parse it as a binary IFC file, resulting in a cryptic 'File too small' error from web-ifc. This change adds detection of .json URLs at the start of the load() method and throws a clear, actionable error message explaining: 1. That .json files cannot be loaded directly 2. That JSON files from ifc-to-json only contain property data, not geometry 3. That users should load the IFC file normally and then use addModelJSONData() for property data 4. A link to the documentation for the correct workflow Fixes ThatOpen/web-ifc-three#136 (bounty) --- web-ifc-three/src/IFCLoader.ts | 30 ++++++++++++++++++++++++++++++ 1 file changed, 30 insertions(+) diff --git a/web-ifc-three/src/IFCLoader.ts b/web-ifc-three/src/IFCLoader.ts index 2a44abd..a7359cb 100644 --- a/web-ifc-three/src/IFCLoader.ts +++ b/web-ifc-three/src/IFCLoader.ts @@ -11,6 +11,13 @@ class IFCLoader extends Loader { this.ifcManager = new IFCManager(); } + /** + * Loads an IFC file from a given URL. + * @param url URL of the IFC file or blob URL. + * @param onLoad Callback when the IFC model is loaded. + * @param onProgress Optional progress callback. + * @param onError Optional error callback. + */ load( url: any, onLoad: (ifc: IFCModel) => void, @@ -19,6 +26,29 @@ class IFCLoader extends Loader { ) { const scope = this; + // Detect JSON files and provide a helpful error message. + // Users often try to load JSON files (produced by ifc-to-json) directly, + // but web-ifc-three only supports binary IFC files for geometry. + const urlString = typeof url === 'string' ? url : ''; + if (urlString.endsWith('.json')) { + const error = new Error( + 'Cannot load ".json" files directly with IFCLoader. ' + + 'The IFCLoader is designed to load binary IFC files (.ifc) for geometry. ' + + 'If you are trying to load a JSON file produced by "ifc-to-json", ' + + 'please note that JSON files contain property/metadata only (not geometry) ' + + 'and must be loaded alongside an IFC file using addModelJSONData(). ' + + 'See the documentation for the correct workflow: ' + + 'https://ifcjs.github.io/info/docs/Guide/web-ifc/Introduction' + ); + if (onError) { + onError(error); + } else { + console.error(error); + } + scope.manager.itemError(url); + return; + } + const loader = new FileLoader(scope.manager); this.onProgress = onProgress; loader.setPath(scope.path); From 79e4da15ecac722baec0c3d4ef9cf0e451a665f7 Mon Sep 17 00:00:00 2001 From: TeapoyY Date: Mon, 6 Apr 2026 08:25:41 +0800 Subject: [PATCH 2/2] fix: filter out NaN vertices from IFC geometry to prevent computeBoundingSphere NaN error When loading certain IFC files, the web-ifc parser can produce geometry with NaN position values. This causes Three.js to throw 'computeBoundingSphere(): Computed radius is NaN' when the geometry's bounding sphere is computed. This fix adds a NaN check in ifcGeometryToBuffer(): 1. First pass: identify vertices with NaN position coordinates 2. Fast path: if all vertices are valid, use the original code 3. Slow path: filter out invalid vertices and remap the index buffer to exclude triangles that reference invalid vertices Fixes ThatOpen/web-ifc-three#88 (bounty) --- web-ifc-three/src/IFC/components/IFCParser.ts | 96 ++++++++++++++++--- 1 file changed, 84 insertions(+), 12 deletions(-) diff --git a/web-ifc-three/src/IFC/components/IFCParser.ts b/web-ifc-three/src/IFC/components/IFCParser.ts index 0a54c1d..ab741b3 100644 --- a/web-ifc-three/src/IFC/components/IFCParser.ts +++ b/web-ifc-three/src/IFC/components/IFCParser.ts @@ -233,20 +233,92 @@ export class IFCParser implements ParserAPI { private ifcGeometryToBuffer(expressID: number, vertexData: Float32Array, indexData: Uint32Array) { const geometry = new BufferGeometry(); - const posFloats = new Float32Array(vertexData.length / 2); - const normFloats = new Float32Array(vertexData.length / 2); - const idAttribute = new Uint32Array(vertexData.length / 6); + const numVertices = vertexData.length / 6; + + // First pass: identify valid vertices (no NaN in position) and build index mapping + const validFlags = new Uint8Array(numVertices); + let validCount = 0; + for (let i = 0; i < numVertices; i++) { + const x = vertexData[i * 6]; + const y = vertexData[i * 6 + 1]; + const z = vertexData[i * 6 + 2]; + if (Number.isFinite(x) && Number.isFinite(y) && Number.isFinite(z)) { + validFlags[i] = 1; + validCount++; + } + } + + // If all vertices are valid, use the fast path (no filtering) + if (validCount === numVertices) { + const posFloats = new Float32Array(vertexData.length / 2); + const normFloats = new Float32Array(vertexData.length / 2); + const idAttribute = new Uint32Array(vertexData.length / 6); + + for (let i = 0; i < vertexData.length; i += 6) { + posFloats[i / 2] = vertexData[i]; + posFloats[i / 2 + 1] = vertexData[i + 1]; + posFloats[i / 2 + 2] = vertexData[i + 2]; + + normFloats[i / 2] = vertexData[i + 3]; + normFloats[i / 2 + 1] = vertexData[i + 4]; + normFloats[i / 2 + 2] = vertexData[i + 5]; + + idAttribute[i / 6] = expressID; + } + + geometry.setAttribute( + 'position', + new BufferAttribute(posFloats, 3)); + geometry.setAttribute( + 'normal', + new BufferAttribute(normFloats, 3)); + geometry.setAttribute( + 'expressID', + new BufferAttribute(idAttribute, 1)); + + geometry.setIndex(new BufferAttribute(indexData, 1)); + return geometry; + } + + // Some vertices have NaN positions — filter them out + // Build remapping: old index -> new index + const indexRemap = new Uint32Array(numVertices); + let nextNewIndex = 0; + for (let i = 0; i < numVertices; i++) { + if (validFlags[i]) { + indexRemap[i] = nextNewIndex++; + } + } - for (let i = 0; i < vertexData.length; i += 6) { - posFloats[i / 2] = vertexData[i]; - posFloats[i / 2 + 1] = vertexData[i + 1]; - posFloats[i / 2 + 2] = vertexData[i + 2]; + const posFloats = new Float32Array(validCount * 3); + const normFloats = new Float32Array(validCount * 3); + const idAttribute = new Uint32Array(validCount); - normFloats[i / 2] = vertexData[i + 3]; - normFloats[i / 2 + 1] = vertexData[i + 4]; - normFloats[i / 2 + 2] = vertexData[i + 5]; + let newVertIdx = 0; + for (let i = 0; i < numVertices; i++) { + if (!validFlags[i]) continue; + posFloats[newVertIdx * 3] = vertexData[i * 6]; + posFloats[newVertIdx * 3 + 1] = vertexData[i * 6 + 1]; + posFloats[newVertIdx * 3 + 2] = vertexData[i * 6 + 2]; - idAttribute[i / 6] = expressID; + normFloats[newVertIdx * 3] = vertexData[i * 6 + 3]; + normFloats[newVertIdx * 3 + 1] = vertexData[i * 6 + 4]; + normFloats[newVertIdx * 3 + 2] = vertexData[i * 6 + 5]; + + idAttribute[newVertIdx] = expressID; + newVertIdx++; + } + + // Remap indices to use new vertex indices, discarding triangles with invalid vertices + const filteredIndices: number[] = []; + for (let i = 0; i < indexData.length; i += 3) { + const a = indexRemap[indexData[i]]; + const b = indexRemap[indexData[i + 1]]; + const c = indexRemap[indexData[i + 2]]; + // Only include the triangle if all three vertices are valid + if (validFlags[indexData[i]] && validFlags[indexData[i + 1]] && validFlags[indexData[i + 2]]) { + filteredIndices.push(a, b, c); + } } geometry.setAttribute( @@ -259,7 +331,7 @@ export class IFCParser implements ParserAPI { 'expressID', new BufferAttribute(idAttribute, 1)); - geometry.setIndex(new BufferAttribute(indexData, 1)); + geometry.setIndex(new BufferAttribute(new Uint32Array(filteredIndices), 1)); return geometry; }