UNPKG

@inweb/viewer-three

Version:

JavaScript library for rendering CAD and BIM files in a browser using Three.js

599 lines (519 loc) 17.6 kB
import { TextureLoader, BufferAttribute, Color, DoubleSide, MeshPhongMaterial, PointsMaterial, LineBasicMaterial, } from "three"; export const GL_COMPONENT_TYPES = { 5120: Int8Array, 5121: Uint8Array, 5122: Int16Array, 5123: Uint16Array, 5125: Uint32Array, 5126: Float32Array, }; export const GL_CONSTANTS = { FLOAT: 5126, //FLOAT_MAT2: 35674, FLOAT_MAT3: 35675, FLOAT_MAT4: 35676, FLOAT_VEC2: 35664, FLOAT_VEC3: 35665, FLOAT_VEC4: 35666, LINEAR: 9729, REPEAT: 10497, SAMPLER_2D: 35678, POINTS: 0, LINES: 1, LINE_LOOP: 2, LINE_STRIP: 3, TRIANGLES: 4, TRIANGLE_STRIP: 5, TRIANGLE_FAN: 6, UNSIGNED_BYTE: 5121, UNSIGNED_SHORT: 5123, }; const MAX_GAP = 128 * 1024; // 128 KB const MAX_CHUNK = 30 * 1024 * 1024; // 100 MB export class GltfStructure { constructor(id, loadController) { this.id = `${id}`; this.json = null; this.loadController = loadController; this.loader = null; this.batchDelay = 10; this.maxBatchSize = 5 * 1024 * 1024; this.maxRangesPerRequest = 512; this.pendingRequests = []; this.batchTimeout = null; this.textureLoader = new TextureLoader(); this.materials = new Map(); this.textureCache = new Map(); this.materialCache = new Map(); this.uri = ""; this._nextObjectId = 1; this.loadingAborted = false; this.criticalError = null; } async initialize(loader) { this.json = await this.loadController.loadJson(); this.loader = loader; this.uri = this.json.buffers[0].uri || ""; } clear() { this.json = null; this.loadController = null; this.pendingRequests = []; if (this.batchTimeout) { clearTimeout(this.batchTimeout); this.batchTimeout = null; } this.disposeMaterials(); this.textureCache.clear(); this.materials.clear(); this.activeChunkLoads = 0; this.chunkQueue = []; this.loadingAborted = false; this.criticalError = null; } getJson() { return this.json; } scheduleRequest(request) { return new Promise((resolve, reject) => { if (this.loadingAborted) { reject(this.criticalError || new Error("Structure loading has been aborted due to critical error")); return; } this.pendingRequests.push({ ...request, _resolve: resolve, _reject: reject, }); }); } isCriticalHttpError(error) { if (!error) return false; const status = error.status || error.statusCode || error.code; if (typeof status === "number") { return status >= 400 && status < 600; } if (error.message) { const match = error.message.match(/HTTP\s+(\d{3})/i); if (match) { const code = parseInt(match[1], 10); return code >= 400 && code < 600; } } return false; } abortLoading(error) { if (this.loadingAborted) { return; } this.loadingAborted = true; this.criticalError = error; const requests = [...this.pendingRequests]; this.pendingRequests = []; for (const req of requests) { if (req._reject) { req._reject(error); } } console.error( `❌ Critical error for structure "${this.id}". All further loading aborted.`, `\n Error: ${error.message || error}`, `\n Rejected ${requests.length} pending chunk requests.` ); throw error; } async flushBufferRequests() { if (!this.pendingRequests || this.pendingRequests.length === 0) return; const requests = [...this.pendingRequests]; this.pendingRequests = []; requests.sort((a, b) => a.offset - b.offset); const mergedRanges = []; let current = { start: requests[0].offset, end: requests[0].offset + requests[0].length, requests: [requests[0]], }; for (let i = 1; i < requests.length; i++) { const req = requests[i]; const gap = req.offset - current.end; const newEnd = Math.max(current.end, req.offset + req.length); const projectedSize = newEnd - current.start; if (gap <= MAX_GAP && projectedSize <= MAX_CHUNK) { current.end = newEnd; current.requests.push(req); } else { mergedRanges.push(current); current = { start: req.offset, end: req.offset + req.length, requests: [req], }; } } mergedRanges.push(current); const finalRanges = []; for (const range of mergedRanges) { let { start, end, requests } = range; while (end - start > MAX_CHUNK) { let splitIdx = 0; for (let i = 0; i < requests.length; i++) { if (requests[i].offset + requests[i].length - start > MAX_CHUNK) { break; } splitIdx = i; } const chunkRequests = requests.slice(0, splitIdx + 1); const chunkEnd = chunkRequests[chunkRequests.length - 1].offset + chunkRequests[chunkRequests.length - 1].length; finalRanges.push({ start, end: chunkEnd, requests: chunkRequests, }); requests = requests.slice(splitIdx + 1); if (requests.length > 0) { start = requests[0].offset; end = requests[0].offset + requests[0].length; for (let i = 1; i < requests.length; i++) { end = Math.max(end, requests[i].offset + requests[i].length); } } } if (requests.length > 0) { finalRanges.push({ start, end, requests }); } } const promises = finalRanges.map(async (range, index) => { if (this.loadingAborted) { for (const req of range.requests) { req._reject(this.criticalError || new Error("Structure loading aborted")); } return; } await this.loader.waitForChunkSlot(); try { const length = range.end - range.start; const buffer = await this.loadController.loadBinaryData([{ offset: range.start, length }], this.uri); for (const req of range.requests) { const relOffset = req.offset - range.start; try { req._resolve({ buffer, relOffset, length: req.length }); } catch (e) { req._reject(e); } } } catch (error) { for (const req of range.requests) { req._reject(error); } if (this.isCriticalHttpError(error)) { this.abortLoading(error); } else { console.warn(`Failed to load chunk ${index + 1}/${finalRanges.length} (${range.start}-${range.end}):`, error); } } finally { this.loader.releaseChunkSlot(); } }); await Promise.all(promises); this.pendingRequests = []; } getBufferView(byteOffset, byteLength, componentType) { return this.scheduleRequest({ offset: byteOffset, length: byteLength, componentType, }); } createTypedArray(buffer, offset, length, componentType) { try { if (!buffer || !(buffer instanceof ArrayBuffer)) { throw new Error("Invalid buffer"); } let elementSize; switch (componentType) { case 5120: case 5121: elementSize = 1; break; // BYTE, UNSIGNED_BYTE case 5122: case 5123: elementSize = 2; break; // SHORT, UNSIGNED_SHORT case 5125: case 5126: elementSize = 4; break; // UNSIGNED_INT, FLOAT default: throw new Error(`Unsupported component type: ${componentType}`); } const numElements = length / elementSize; if (!Number.isInteger(numElements)) { throw new Error(`Invalid length ${length} for component type ${componentType}`); } if (length > buffer.byteLength) { throw new Error(`Buffer too small: need ${length} bytes, but buffer is ${buffer.byteLength} bytes`); } const ArrayType = GL_COMPONENT_TYPES[componentType]; return new ArrayType(buffer, offset, numElements); } catch (error) { if (error.name !== "AbortError") { console.error("Error creating typed array:", { bufferSize: buffer?.byteLength, offset, length, componentType, error, }); } throw error; } } async createBufferAttribute(accessorIndex) { if (!this.json) { throw new Error("No GLTF structure loaded"); } const gltf = this.json; const accessor = gltf.accessors[accessorIndex]; const bufferView = gltf.bufferViews[accessor.bufferView]; try { const byteOffset = (bufferView.byteOffset || 0) + (accessor.byteOffset || 0); const components = this.getNumComponents(accessor.type); const count = accessor.count; const byteLength = count * components * this.getComponentSize(accessor.componentType); const array = await this.getBufferView(byteOffset, byteLength, accessor.componentType); const attribute = new BufferAttribute(array, components); if (accessor.min !== undefined) attribute.min = accessor.min; if (accessor.max !== undefined) attribute.max = accessor.max; return attribute; } catch (error) { if (error.name !== "AbortError") { console.error("Error creating buffer attribute:", { error, accessor, bufferView, }); } throw error; } } getComponentSize(componentType) { switch (componentType) { case 5120: // BYTE case 5121: // UNSIGNED_BYTE return 1; case 5122: // SHORT case 5123: // UNSIGNED_SHORT return 2; case 5125: // UNSIGNED_INT case 5126: // FLOAT return 4; default: throw new Error(`Unknown component type: ${componentType}`); } } getNumComponents(type) { switch (type) { case "SCALAR": return 1; case "VEC2": return 2; case "VEC3": return 3; case "VEC4": return 4; case "MAT2": return 4; case "MAT3": return 9; case "MAT4": return 16; default: throw new Error(`Unknown type: ${type}`); } } async loadTextures() { if (!this.json.textures) return; const loadTexture = async (imageIndex) => { const image = this.json.images[imageIndex]; if (image.uri) { const fullUrl = await this.loadController.resolveURL(image.uri); return this.textureLoader.loadAsync(fullUrl); } else if (image.bufferView !== undefined) { const bufferView = this.json.bufferViews[image.bufferView]; const array = await this.getBufferView(bufferView.byteOffset || 0, bufferView.byteLength, 5121); const blob = new Blob([array], { type: image.mimeType }); const url = URL.createObjectURL(blob); const texture = await this.textureLoader.loadAsync(url); URL.revokeObjectURL(url); texture.flipY = false; return texture; } }; const texturePromises = []; for (let i = 0; i < this.json.textures.length; i++) { texturePromises.push( loadTexture(this.json.textures[i].source).then((texture) => this.textureCache.set(i, texture)) ); } await Promise.all(texturePromises); } loadMaterials() { if (!this.json.materials) return this.materials; for (let i = 0; i < this.json.materials.length; i++) { const materialDef = this.json.materials[i]; const materialCacheKey = `material_${i}`; this.materialCache.set(materialCacheKey, { mesh: this.createMaterial(materialDef, GL_CONSTANTS.TRIANGLES), points: this.createMaterial(materialDef, GL_CONSTANTS.POINTS), lines: this.createMaterial(materialDef, GL_CONSTANTS.LINES), }); this.materialCache.get(materialCacheKey).mesh.name = materialDef.name; this.materialCache.get(materialCacheKey).points.name = materialDef.name; this.materialCache.get(materialCacheKey).lines.name = materialDef.name; this.materials.set(i, this.materialCache.get(materialCacheKey).mesh); } return this.materials; } createMaterial(materialDef, primitiveMode = undefined) { const params = {}; if (materialDef.pbrMetallicRoughness) { const pbr = materialDef.pbrMetallicRoughness; if (pbr.baseColorFactor) { params.color = new Color().fromArray(pbr.baseColorFactor); params.opacity = pbr.baseColorFactor[3]; if (params.opacity < 1.0) params.transparent = true; } if (pbr.baseColorTexture) { params.map = this.textureCache.get(pbr.baseColorTexture.index); } } if (materialDef.emissiveFactor) { params.emissive = new Color().fromArray(materialDef.emissiveFactor); } if (materialDef.alphaMode === "BLEND") { params.transparent = true; } if (materialDef.doubleSided) { params.side = DoubleSide; } let material; if (primitiveMode === GL_CONSTANTS.POINTS) { params.sizeAttenuation = false; material = new PointsMaterial(params); material.sizeAttenuation = false; } else if ( primitiveMode === GL_CONSTANTS.LINES || primitiveMode === GL_CONSTANTS.LINE_STRIP || primitiveMode === GL_CONSTANTS.LINE_LOOP ) { material = new LineBasicMaterial(params); } else { params.specular = 0x222222; params.shininess = 10; params.reflectivity = 0.05; params.polygonOffset = true; params.polygonOffsetFactor = 1; params.polygonOffsetUnits = 1; if (materialDef.normalTexture) { params.normalMap = this.textureCache.get(materialDef.normalTexture.index); } material = new MeshPhongMaterial(params); } return material; } getCachedMaterial(materialIndex, primitiveMode) { const materialCacheKey = `material_${materialIndex}`; const materialCache = this.materialCache.get(materialCacheKey); if (materialCache) { if (primitiveMode === GL_CONSTANTS.POINTS) { return materialCache.points; } else if ( primitiveMode === GL_CONSTANTS.LINES || primitiveMode === GL_CONSTANTS.LINE_STRIP || primitiveMode === GL_CONSTANTS.LINE_LOOP ) { return materialCache.lines; } else { return materialCache.mesh; } } return null; } disposeMaterials() { this.textureCache.forEach((texture) => texture.dispose()); this.textureCache.clear(); this.materials.forEach((material) => { if (material.map) material.map.dispose(); if (material.lightMap) material.lightMap.dispose(); if (material.bumpMap) material.bumpMap.dispose(); if (material.normalMap) material.normalMap.dispose(); if (material.specularMap) material.specularMap.dispose(); if (material.envMap) material.envMap.dispose(); if (material.aoMap) material.aoMap.dispose(); if (material.metalnessMap) material.metalnessMap.dispose(); if (material.roughnessMap) material.roughnessMap.dispose(); if (material.emissiveMap) material.emissiveMap.dispose(); material.dispose(); }); this.materials.clear(); this.materialCache.forEach((materialCache) => { if (materialCache.mesh) { if (materialCache.mesh.map) materialCache.mesh.map.dispose(); if (materialCache.mesh.lightMap) materialCache.mesh.lightMap.dispose(); if (materialCache.mesh.bumpMap) materialCache.mesh.bumpMap.dispose(); if (materialCache.mesh.normalMap) materialCache.mesh.normalMap.dispose(); if (materialCache.mesh.specularMap) materialCache.mesh.specularMap.dispose(); if (materialCache.mesh.envMap) materialCache.mesh.envMap.dispose(); if (materialCache.mesh.aoMap) materialCache.mesh.aoMap.dispose(); if (materialCache.mesh.metalnessMap) materialCache.mesh.metalnessMap.dispose(); if (materialCache.mesh.roughnessMap) materialCache.mesh.roughnessMap.dispose(); if (materialCache.mesh.emissiveMap) materialCache.mesh.emissiveMap.dispose(); materialCache.mesh.dispose(); } if (materialCache.points) { if (materialCache.points.map) materialCache.points.map.dispose(); materialCache.points.dispose(); } if (materialCache.lines) { if (materialCache.lines.map) materialCache.lines.map.dispose(); materialCache.lines.dispose(); } }); this.materialCache.clear(); } estimateNodeSize(meshIndex) { if (!this.json.meshes) return 0; const meshDef = this.json.meshes[meshIndex]; if (!meshDef || !meshDef.primitives) return 0; let totalSize = 0; for (const primitive of meshDef.primitives) { if (primitive.attributes) { for (const [, accessorIndex] of Object.entries(primitive.attributes)) { if (accessorIndex === undefined) continue; const accessor = this.json.accessors[accessorIndex]; if (!accessor) continue; const numComponents = this.getNumComponents(accessor.type); const bytesPerComponent = this.getComponentSize(accessor.componentType); totalSize += accessor.count * numComponents * bytesPerComponent; } } if (primitive.indices !== undefined) { const accessor = this.json.accessors[primitive.indices]; if (accessor) { const bytesPerComponent = this.getComponentSize(accessor.componentType); totalSize += accessor.count * bytesPerComponent; } } } return totalSize; } }