UNPKG

@inweb/viewer-three

Version:

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

1,611 lines (1,359 loc) 75.9 kB
import { BufferGeometry, PointsMaterial, Points, Mesh, TriangleStripDrawMode, TriangleFanDrawMode, LineSegments, Line, LineLoop, Group, Vector3, Vector2, Quaternion, Matrix4, Box3, MeshPhongMaterial, Color, MathUtils, PerspectiveCamera, OrthographicCamera, DoubleSide, NormalBlending, BufferAttribute, LineBasicMaterial, } from "three"; import { GL_CONSTANTS } from "./GltfStructure.js"; import { mergeGeometries } from "three/examples/jsm/utils/BufferGeometryUtils.js"; const STRUCTURE_ID_SEPARATOR = ":"; //#AI-GENERATED using Gemini 2.5 Pro, Claude-4-sonnet //#Reviewed and adapted by dborysov@opendesign.com export class DynamicGltfLoader { constructor(camera, scene, renderer) { this.camera = camera; this.scene = scene; this.renderer = renderer; this.eventHandlers = { geometryprogress: [], databasechunk: [], geometryend: [], geometryerror: [], update: [], geometrymemory: [], optimizationprogress: [], }; this.loadDistance = 100; this.unloadDistance = 150; this.checkInterval = 1000; this.nodes = new Map(); this.loadedMeshes = new Map(); this.nodesToLoad = []; this.edgeNodes = []; this.structures = []; this.structureRoots = new Map(); this.memoryLimit = this.getAvailableMemory(); this.loadedGeometrySize = 0; this.geometryCache = new Map(); this.materialCache = new Map(); this.textureCache = new Map(); this.currentMemoryUsage = 0; this.updateMemoryIndicator(); this.loadedMaterials = new Map(); this.abortController = new AbortController(); this.batchSize = 10000; this.frameDelay = 0; this.graphicsObjectLimit = 10000; this.totalLoadedObjects = 0; this.handleToObjects = new Map(); this.originalObjects = new Set(); this.originalObjectsToSelection = new Set(); this.optimizedOriginalMap = new Map(); this.mergedMesh = new Set(); this.mergedLines = new Set(); this.mergedLineSegments = new Set(); this.mergedPoints = new Set(); this.isolatedObjects = []; //!!window.WebGL2RenderingContext && this.renderer.getContext() instanceof WebGL2RenderingContext this.useVAO = false; this.visibleEdges = true; this.handleToOptimizedObjects = new Map(); this.hiddenHandles = new Set(); this.newOptimizedObjects = new Set(); this.oldOptimizeObjects = new Set(); // Transform system for exploded view - works directly with original objects this.objectTransforms = new Map(); // originalObject -> Matrix4 this.transformedGeometries = new Map(); // mergedObject.uuid -> original position data this.activeChunkLoads = 0; this.chunkQueue = []; // GPU-accelerated visibility system this.objectIdToIndex = new Map(); // objectId -> index this.maxObjectId = 0; // Maximum object ID this.objectVisibility = new Float32Array(); // Array of visibility flags for each object // Chunk loading configuration this.maxConcurrentChunks = 6; // Default limit // Merged geometry tracking this.mergedObjectMap = new Map(); // objectId -> {mergedObject, startIndex, endIndex, vertexCount} this.mergedGeometryVisibility = new Map(); // mergedObject -> visibility array this._webglInfoCache = null; } setVisibleEdges(visible) { this.visibleEdges = visible; } getAvailableMemory() { let memoryLimit = 6 * 1024 * 1024 * 1024; try { if (navigator.deviceMemory) { memoryLimit = navigator.deviceMemory * 1024 * 1024 * 1024; } else if (performance.memory) { const jsHeapSizeLimit = performance.memory.jsHeapSizeLimit; if (jsHeapSizeLimit) { memoryLimit = Math.min(memoryLimit, jsHeapSizeLimit); } } memoryLimit = Math.min(memoryLimit, 16 * 1024 * 1024 * 1024); memoryLimit = Math.max(memoryLimit, 2 * 1024 * 1024 * 1024); console.log(`Available memory set to ${Math.round(memoryLimit / (1024 * 1024 * 1024))}GB`); } catch (error) { console.warn("Error detecting available memory:", error); } return memoryLimit / 3; } getAbortController() { return this.abortController; } abortLoading() { this.abortController.abort(); } updateMemoryIndicator() { this.dispatchEvent("geometrymemory", { currentUsage: this.currentMemoryUsage, limit: this.memoryLimit, }); } setMemoryLimit(bytesLimit) { // this.memoryLimit = bytesLimit; //this.updateMemoryIndicator(); // console.log(`Memory limit set to ${Math.round(bytesLimit / (1024 * 1024))}MB`); } estimateGeometrySize(nodeGroup) { let totalSize = 0; nodeGroup.traverse((child) => { if (child.geometry) { if (this.abortController.signal.aborted) { throw new DOMException("Loading aborted", "AbortError"); } const geometry = child.geometry; if (geometry.attributes) { Object.values(geometry.attributes).forEach((attribute) => { if (attribute && attribute.array) { totalSize += attribute.array.byteLength; } }); } if (geometry.index && geometry.index.array) { totalSize += geometry.index.array.byteLength; } } }); return totalSize; } recalculateScene() { const geometries = []; this.scene.traverse((object) => { if (this.abortController.signal.aborted) { throw new DOMException("Loading aborted", "AbortError"); } if (object.geometry && !this.geometryCache.has(object.geometry.uuid)) { const size = this.estimateGeometrySize(object); this.geometryCache.set(object.geometry.uuid, size); geometries.push({ object, size, distance: object.position.distanceTo(this.camera.position), }); } }); if (this.abortController.signal.aborted) { throw new DOMException("Loading aborted", "AbortError"); } geometries.sort((a, b) => b.distance - a.distance); let currentMemoryUsage = 0; for (const geo of geometries) { currentMemoryUsage += geo.size; } if (currentMemoryUsage > this.memoryLimit) { console.log(`Memory usage (${Math.round(currentMemoryUsage / (1024 * 1024))}MB) exceeds limit`); for (const geo of geometries) { if (currentMemoryUsage <= this.memoryLimit) break; if (this.abortController.signal.aborted) { throw new DOMException("Loading aborted", "AbortError"); } const object = geo.object; if (object.geometry) { currentMemoryUsage -= geo.size; this.geometryCache.delete(object.geometry.uuid); object.geometry.dispose(); object.visible = false; } } } this.currentMemoryUsage = currentMemoryUsage; this.updateMemoryIndicator(); console.log(`Final memory usage: ${Math.round(currentMemoryUsage / (1024 * 1024))}MB`); } getStats() { let totalObjects = 0; let renderedObjects = 0; let totalTriangles = 0; let renderedTriangles = 0; let totalLines = 0; let renderedLines = 0; let totalEdges = 0; let renderedEdges = 0; this.scene.traverse((object) => { totalObjects++; const geometry = object.geometry; if (!geometry) return; let triCount = 0; if (geometry.index) { triCount = Math.floor(geometry.index.count / 3); } else if (geometry.attributes && geometry.attributes.position) { triCount = Math.floor(geometry.attributes.position.count / 3); } totalTriangles += triCount; let lineCount = 0; if (geometry.index) { lineCount = Math.floor(geometry.index.count / 2); } else if (geometry.attributes && geometry.attributes.position) { lineCount = Math.floor(geometry.attributes.position.count / 2); } if (object.type === "Line" || object.type === "LineSegments" || object.type === "LineLoop") { if (object.userData.isEdge) { totalEdges += lineCount; } else { totalLines += lineCount; } } if (object.visible !== false) { if (object.isMesh || object.isLine || object.isPoints) { renderedObjects++; if (object.isMesh) { renderedTriangles += triCount; } else if (object.type === "Line" || object.type === "LineSegments" || object.type === "LineLoop") { if (object.userData.isEdge) { renderedEdges += lineCount; } else { renderedLines += lineCount; } } } } }); const geometryCount = this.geometryCache ? this.geometryCache.size : 0; const geometryMemoryBytes = Array.from(this.geometryCache?.values?.() || []).reduce((a, b) => a + b, 0); const uniqueMaterialIds = new Set(); const uniqueTextureIds = new Set(); if (Array.isArray(this.structures)) { for (const structure of this.structures) { try { for (const entry of structure.materialCache.values()) { if (entry?.mesh?.uuid) uniqueMaterialIds.add(entry.mesh.uuid); if (entry?.points?.uuid) uniqueMaterialIds.add(entry.points.uuid); if (entry?.lines?.uuid) uniqueMaterialIds.add(entry.lines.uuid); } } catch (exp) { console.error("Error adding material to uniqueMaterialIds", exp); } } } const materialCount = uniqueMaterialIds.size; const textureCount = uniqueTextureIds.size; const estimatedGpuMemoryBytes = geometryMemoryBytes; if (!this._webglInfoCache) { try { const gl = this.renderer.getContext(); const dbgInfo = gl.getExtension("WEBGL_debug_renderer_info"); if (dbgInfo) { const rendererStr = gl.getParameter(dbgInfo.UNMASKED_RENDERER_WEBGL); const vendorStr = gl.getParameter(dbgInfo.UNMASKED_VENDOR_WEBGL); this._webglInfoCache = { renderer: rendererStr, vendor: vendorStr }; } else { this._webglInfoCache = { renderer: null, vendor: null }; } } catch (e) { console.error("Error getting webgl info", e); this._webglInfoCache = { renderer: null, vendor: null }; } } const size = new Vector2(); if (this.renderer && this.renderer.getSize) { this.renderer.getSize(size); } return { scene: { beforeOptimization: { objects: totalObjects - renderedObjects, triangles: totalTriangles - renderedTriangles, lines: totalLines - renderedLines, edges: totalEdges - renderedEdges, }, afterOptimization: { objects: renderedObjects, triangles: renderedTriangles, lines: renderedLines, edges: renderedEdges, }, }, memory: { geometries: { count: geometryCount, bytes: geometryMemoryBytes }, textures: { count: textureCount }, materials: { count: materialCount }, totalEstimatedGpuBytes: estimatedGpuMemoryBytes, }, system: { webglRenderer: this._webglInfoCache?.renderer || "", webglVendor: this._webglInfoCache?.vendor || "", viewport: { width: size.x || 0, height: size.y || 0 }, }, }; } async loadNode(nodeId, onLoadFinishCb) { const node = this.nodes.get(nodeId); if (!node || node.loaded || node.loading) return; node.loading = true; const meshDef = node.structure.getJson().meshes[node.meshIndex]; try { const bufferRequests = []; const primitiveReqMap = new Map(); for (let primIdx = 0; primIdx < meshDef.primitives.length; primIdx++) { const primitive = meshDef.primitives[primIdx]; const reqs = []; if (primitive.attributes.POSITION !== undefined) { const accessorIndex = primitive.attributes.POSITION; const accessor = node.structure.json.accessors[accessorIndex]; const bufferView = node.structure.json.bufferViews[accessor.bufferView]; const byteOffset = (bufferView.byteOffset || 0) + (accessor.byteOffset || 0); const components = node.structure.getNumComponents(accessor.type); const count = accessor.count; const byteLength = count * components * node.structure.getComponentSize(accessor.componentType); reqs.push({ offset: byteOffset, length: byteLength, componentType: accessor.componentType, accessorIndex, type: "position", primIdx, }); } if (primitive.attributes.NORMAL !== undefined) { const accessorIndex = primitive.attributes.NORMAL; const accessor = node.structure.json.accessors[accessorIndex]; const bufferView = node.structure.json.bufferViews[accessor.bufferView]; const byteOffset = (bufferView.byteOffset || 0) + (accessor.byteOffset || 0); const components = node.structure.getNumComponents(accessor.type); const count = accessor.count; const byteLength = count * components * node.structure.getComponentSize(accessor.componentType); reqs.push({ offset: byteOffset, length: byteLength, componentType: accessor.componentType, accessorIndex, type: "normal", primIdx, }); } if (primitive.attributes.TEXCOORD_0 !== undefined) { const accessorIndex = primitive.attributes.TEXCOORD_0; const accessor = node.structure.json.accessors[accessorIndex]; const bufferView = node.structure.json.bufferViews[accessor.bufferView]; const byteOffset = (bufferView.byteOffset || 0) + (accessor.byteOffset || 0); const components = node.structure.getNumComponents(accessor.type); const count = accessor.count; const byteLength = count * components * node.structure.getComponentSize(accessor.componentType); reqs.push({ offset: byteOffset, length: byteLength, componentType: accessor.componentType, accessorIndex, type: "uv", primIdx, }); } if (primitive.indices !== undefined) { const accessorIndex = primitive.indices; const accessor = node.structure.json.accessors[accessorIndex]; const bufferView = node.structure.json.bufferViews[accessor.bufferView]; const byteOffset = (bufferView.byteOffset || 0) + (accessor.byteOffset || 0); const components = node.structure.getNumComponents(accessor.type); const count = accessor.count; const byteLength = count * components * node.structure.getComponentSize(accessor.componentType); reqs.push({ offset: byteOffset, length: byteLength, componentType: accessor.componentType, accessorIndex, type: "index", primIdx, }); } primitiveReqMap.set(primIdx, reqs); bufferRequests.push(...reqs); } if (bufferRequests.length === 0) { node.loaded = true; node.loading = false; return; } bufferRequests.sort((a, b) => a.offset - b.offset); const minOffset = bufferRequests[0].offset; const maxOffset = Math.max(...bufferRequests.map((r) => r.offset + r.length)); const totalLength = maxOffset - minOffset; const { buffer, relOffset: baseRelOffset } = await node.structure.scheduleRequest({ offset: minOffset, length: totalLength, componentType: null, }); for (const req of bufferRequests) { const relOffset = req.offset - minOffset; req.data = node.structure.createTypedArray(buffer, baseRelOffset + relOffset, req.length, req.componentType); } for (let primIdx = 0; primIdx < meshDef.primitives.length; primIdx++) { const primitive = meshDef.primitives[primIdx]; const geometry = new BufferGeometry(); const reqs = primitiveReqMap.get(primIdx); if (primitive.attributes.POSITION !== undefined) { const req = reqs.find((r) => r.type === "position" && r.accessorIndex === primitive.attributes.POSITION); const accessor = node.structure.json.accessors[primitive.attributes.POSITION]; const components = node.structure.getNumComponents(accessor.type); geometry.setAttribute("position", new BufferAttribute(req.data, components)); } if (primitive.attributes.NORMAL !== undefined) { const req = reqs.find((r) => r.type === "normal" && r.accessorIndex === primitive.attributes.NORMAL); const accessor = node.structure.json.accessors[primitive.attributes.NORMAL]; const components = node.structure.getNumComponents(accessor.type); geometry.setAttribute("normal", new BufferAttribute(req.data, components)); } if (primitive.attributes.TEXCOORD_0 !== undefined) { const req = reqs.find((r) => r.type === "uv" && r.accessorIndex === primitive.attributes.TEXCOORD_0); const accessor = node.structure.json.accessors[primitive.attributes.TEXCOORD_0]; const components = node.structure.getNumComponents(accessor.type); geometry.setAttribute("uv", new BufferAttribute(req.data, components)); } if (primitive.indices !== undefined) { const req = reqs.find((r) => r.type === "index" && r.accessorIndex === primitive.indices); geometry.setIndex(new BufferAttribute(req.data, 1)); } let material; if (primitive.material !== undefined) { material = node.structure.getCachedMaterial(primitive.material, primitive.mode); if (!material) { const materialDef = node.structure.json.materials[primitive.material]; material = node.structure.createMaterial(materialDef, primitive.mode); } } else { material = this.createDefaultMaterial(primitive.mode); } let mesh; if (primitive.mode === GL_CONSTANTS.POINTS) { mesh = new Points(geometry, material); } else if ( primitive.mode === GL_CONSTANTS.TRIANGLES || primitive.mode === GL_CONSTANTS.TRIANGLE_STRIP || primitive.mode === GL_CONSTANTS.TRIANGLE_FAN || primitive.mode === undefined ) { mesh = new Mesh(geometry, material); if (primitive.mode === GL_CONSTANTS.TRIANGLE_STRIP) { mesh.drawMode = TriangleStripDrawMode; } else if (primitive.mode === GL_CONSTANTS.TRIANGLE_FAN) { mesh.drawMode = TriangleFanDrawMode; } } else if (primitive.mode === GL_CONSTANTS.LINES) { mesh = new LineSegments(geometry, material); } else if (primitive.mode === GL_CONSTANTS.LINE_STRIP) { mesh = new Line(geometry, material); } else if (primitive.mode === GL_CONSTANTS.LINE_LOOP) { mesh = new LineLoop(geometry, material); } if (node.extras) { mesh.userData = { ...mesh.userData, ...node.extras }; } if (meshDef.extras) { mesh.userData = { ...mesh.userData, ...meshDef.extras }; } if (primitive.extras) { mesh.userData = { ...mesh.userData, ...primitive.extras }; } if (node.handle) { mesh.userData.handle = node.handle; } else { mesh.userData.handle = this.getFullHandle(node.structure.id, mesh.userData.handle); } if (mesh.material.name === "edges") { mesh.userData.isEdge = true; } else { mesh.userData.isEdge = false; } this.registerObjectWithHandle(mesh, mesh.userData.handle); mesh.position.copy(node.position); if (!geometry.attributes.normal) { geometry.computeVertexNormals(); } if (material.aoMap && geometry.attributes.uv) { geometry.setAttribute("uv2", geometry.attributes.uv); } if (node.group) { node.group.add(mesh); } else { this.scene.add(mesh); } node.object = mesh; this.totalLoadedObjects++; mesh.visible = this.totalLoadedObjects < this.graphicsObjectLimit; } node.loaded = true; node.loading = false; const geometrySize = this.estimateGeometrySize(node.object); this.geometryCache.set(node.object.uuid, geometrySize); this.currentMemoryUsage += geometrySize; if (onLoadFinishCb) { onLoadFinishCb(); } } catch (error) { node.loading = false; if (error.name === "AbortError") { return; } if (node.structure && node.structure.loadingAborted) { return; } console.error(`Error loading node ${nodeId}:`, error); } } unloadNode(nodeId) { const node = this.nodes.get(nodeId); if (!node || !node.loaded) return; if (node.object) { if (node.object.parent) { node.object.parent.remove(node.object); } else { this.scene.remove(node.object); } node.object.traverse((child) => { if (child.geometry) { const geometrySize = this.geometryCache.get(child.geometry.uuid) || 0; this.currentMemoryUsage -= geometrySize; this.geometryCache.delete(child.geometry.uuid); child.geometry.dispose(); } }); node.object = null; node.loaded = false; this.updateMemoryIndicator(); console.log(`Unloaded node: ${nodeId}`); } } checkDistances() { const cameraPosition = this.camera.position; this.nodes.forEach((node, nodeId) => { const distance = cameraPosition.distanceTo(node.position); if (node.loaded) { if (distance > this.unloadDistance) { this.unloadNode(nodeId); } } else if (!node.loading) { if (distance < this.loadDistance) { this.loadNode(nodeId); } } }); } async loadStructure(structures) { this.clear(); const structureArray = Array.isArray(structures) ? structures : [structures]; for (const structure of structureArray) { await structure.initialize(this); this.structures.push(structure); } for (const structure of this.structures) { try { await structure.loadTextures(); await structure.loadMaterials(); } catch (error) { console.error("Error loading materials:", error); throw error; } } await this.processSceneHierarchy(); } async processSceneHierarchy() { if (this.structures.length === 0) { throw new Error("No GLTF structures loaded"); } this.nodesToLoad = []; let estimatedSize = 0; for (const structure of this.structures) { const gltf = structure.getJson(); if (!gltf.scenes || !gltf.scenes.length) { console.warn("No scenes found in GLTF structure"); continue; } estimatedSize += gltf.buffers[0].byteLength; const rootGroup = new Group(); rootGroup.name = `structure_${structure.id}_root`; this.scene.add(rootGroup); this.structureRoots.set(structure.id, rootGroup); const scene = gltf.scenes[gltf.scene || 0]; for (const nodeIndex of scene.nodes) { await this.processNodeHierarchy(structure, nodeIndex, rootGroup); } } const ignoreEdges = estimatedSize * 2 > this.memoryLimit; this.nodesToLoad.sort((a, b) => { const nodeA = this.nodes.get(a); const nodeB = this.nodes.get(b); if (!nodeA?.geometryExtents || !nodeB?.geometryExtents) { return 0; } const sizeA = nodeA.geometryExtents.getSize(new Vector3()); const sizeB = nodeB.geometryExtents.getSize(new Vector3()); const volumeA = sizeA.x * sizeA.y * sizeA.z; const volumeB = sizeB.x * sizeB.y * sizeB.z; return volumeB - volumeA; }); if (!ignoreEdges && this.visibleEdges) { this.nodesToLoad.push(...this.edgeNodes); } this.dispatchEvent("databasechunk", { totalNodes: this.nodesToLoad.length, structures: this.structures.map((s) => ({ id: s.id, nodeCount: this.nodesToLoad.filter((nodeId) => nodeId.startsWith(s.id)).length, })), }); } getFullHandle(structureId, originalHandle) { return `${structureId}${STRUCTURE_ID_SEPARATOR}${originalHandle}`; } async processNodeHierarchy(structure, nodeId, parentGroup) { const nodeDef = structure.json.nodes[nodeId]; let nodeGroup = null; let handle = null; if (nodeDef.extras?.handle) { handle = this.getFullHandle(structure.id, nodeDef.extras.handle); } if (nodeDef.camera !== undefined) { const camera = this.loadCamera(structure, nodeDef.camera, nodeDef); if (nodeDef.extras) { camera.userData = { ...camera.userData, ...nodeDef.extras }; } this.scene.add(camera); return; } const needsGroup = this.needsGroupForNode(structure, nodeDef); if (needsGroup) { nodeGroup = new Group(); nodeGroup.name = nodeDef.name || `node_${nodeId}`; if (nodeDef.extras) { nodeGroup.userData = { ...nodeDef.extras }; if (nodeGroup.userData.handle) { nodeGroup.userData.handle = this.getFullHandle(structure.id, nodeGroup.userData.handle); } } if (nodeDef.matrix) { nodeGroup.matrix.fromArray(nodeDef.matrix); nodeGroup.matrixAutoUpdate = false; } else if (nodeDef.translation || nodeDef.rotation || nodeDef.scale) { const position = nodeDef.translation ? new Vector3().fromArray(nodeDef.translation) : new Vector3(); const quaternion = nodeDef.rotation ? new Quaternion().fromArray(nodeDef.rotation) : new Quaternion(); const scale = nodeDef.scale ? new Vector3().fromArray(nodeDef.scale) : new Vector3(1, 1, 1); nodeGroup.matrix.compose(position, quaternion, scale); nodeGroup.matrixAutoUpdate = false; } if (parentGroup) { parentGroup.add(nodeGroup); } } if (nodeDef.mesh !== undefined) { const nodeMatrix = new Matrix4(); const uniqueNodeId = `${structure.id}_${nodeId}`; const meshDef = structure.json.meshes[nodeDef.mesh]; const geometryExtents = new Box3(); for (const primitive of meshDef.primitives) { const positionAccessor = structure.json.accessors[primitive.attributes.POSITION]; if (positionAccessor && positionAccessor.min && positionAccessor.max) { const primitiveBox = new Box3( new Vector3().fromArray(positionAccessor.min), new Vector3().fromArray(positionAccessor.max) ); geometryExtents.union(primitiveBox); } } let isEdge = false; if (meshDef.primitives[0].material !== undefined) { const material = structure.json.materials[meshDef.primitives[0].material]; if (material?.name === "edges") { isEdge = true; } } if (!isEdge) { this.nodesToLoad.push(uniqueNodeId); } else { this.edgeNodes.push(uniqueNodeId); } if (meshDef.extras && meshDef.extras.handle) { handle = this.getFullHandle(structure.id, meshDef.extras.handle); } this.nodes.set(uniqueNodeId, { position: nodeGroup ? nodeGroup.position.clone() : new Vector3().setFromMatrixPosition(nodeMatrix), nodeIndex: nodeId, meshIndex: nodeDef.mesh, loaded: false, loading: false, object: null, group: nodeGroup || parentGroup, structure, extras: nodeDef.extras, geometryExtents, handle: handle || this.getFullHandle(structure.id, structure._nextObjectId++), }); } if (nodeDef.children) { for (const childId of nodeDef.children) { await this.processNodeHierarchy(structure, childId, nodeGroup || parentGroup); } } return nodeGroup; } needsGroupForNode(structure, nodeDef) { const hasTransforms = nodeDef.matrix || nodeDef.translation || nodeDef.rotation || nodeDef.scale; const hasMultiplePrimitives = nodeDef.mesh !== undefined && structure.json.meshes[nodeDef.mesh].primitives.length > 1; return hasTransforms !== undefined || hasMultiplePrimitives; } async processNodes() { const nodesToLoad = this.nodesToLoad; let loadedCount = 0; let lastLoadedCount = 0; const totalNodes = nodesToLoad.length; const loadProgress = async () => { loadedCount++; if (loadedCount - lastLoadedCount > 1000) { lastLoadedCount = loadedCount; this.updateMemoryIndicator(); this.dispatchEvent("geometryprogress", { percentage: Math.round((loadedCount / totalNodes) * 100), loaded: loadedCount, total: totalNodes, }); this.dispatchEvent("update"); await new Promise((resolve) => { setTimeout(resolve, 0); }); } }; try { const loadOperations = []; for (const nodeId of nodesToLoad) { if (this.abortController.signal.aborted) { throw new DOMException("Loading aborted", "AbortError"); } const estimatedSize = await this.estimateNodeSize(nodeId); if (this.currentMemoryUsage + estimatedSize > this.memoryLimit) { console.log(`Memory limit reached after loading ${loadedCount} nodes`); this.dispatchEvent("geometryerror", { message: "Memory limit reached", }); this.dispatchEvent("update"); return loadedCount; } loadOperations.push(this.loadNode(nodeId, loadProgress)); } for (const structure of this.structures) { loadOperations.push(structure.flushBufferRequests()); } await Promise.all(loadOperations); this.dispatchEvent("geometryend", { totalLoaded: loadedCount, totalNodes, }); return loadedCount; } catch (error) { this.dispatchEvent("geometryerror", { error }); throw error; } } async loadNodes() { console.time("Process nodes"); await this.processNodes(); console.timeEnd("Process nodes"); console.time("Optimize scene"); await this.optimizeScene(); console.timeEnd("Optimize scene"); } cleanupPartialLoad() { this.nodesToLoad.forEach((nodeId) => { const node = this.nodes.get(nodeId); if (node && node.loading) { this.unloadNode(nodeId); } }); } createDefaultMaterial(primitiveMode = undefined) { if (primitiveMode === GL_CONSTANTS.POINTS) { return new PointsMaterial({ color: new Color(0x808080), size: 0.05, sizeAttenuation: false, alphaTest: 0.5, transparent: true, vertexColors: false, blending: NormalBlending, depthWrite: false, depthTest: true, }); } else if ( primitiveMode === GL_CONSTANTS.LINES || primitiveMode === GL_CONSTANTS.LINE_STRIP || primitiveMode === GL_CONSTANTS.LINE_LOOP ) { return new LineBasicMaterial({ color: 0x808080, linewidth: 1.0, alphaTest: 0.1, depthTest: true, depthWrite: true, transparent: true, opacity: 1.0, }); } else { return new MeshPhongMaterial({ color: 0x808080, specular: 0x222222, shininess: 10, side: DoubleSide, }); } } async estimateNodeSize(nodeId) { const node = this.nodes.get(nodeId); if (!node) return 0; return await node.structure.estimateNodeSize(node.meshIndex); } getTotalGeometryExtent() { const totalExtent = new Box3(); for (const node of this.nodes.values()) { if (!node.geometryExtents) continue; if (node.object && this.hiddenHandles.has(node.object.userData.handle)) continue; const transformedBox = node.geometryExtents.clone(); const structureRoot = node.structure ? this.structureRoots.get(node.structure.id) : null; if (node.group) { const matrices = []; let currentGroup = node.group; while (currentGroup && currentGroup !== structureRoot) { if (currentGroup.matrix && currentGroup.matrixAutoUpdate === false) { matrices.unshift(currentGroup.matrix); } currentGroup = currentGroup.parent; } for (const matrix of matrices) { transformedBox.applyMatrix4(matrix); } } if (structureRoot && structureRoot.matrix) { transformedBox.applyMatrix4(structureRoot.matrix); } const transform = this.objectTransforms.get(node.object); if (transform) { transformedBox.applyMatrix4(transform); } totalExtent.union(transformedBox); } if (this.scene && this.scene.matrix && !totalExtent.isEmpty()) { totalExtent.applyMatrix4(this.scene.matrix); } return totalExtent; } loadCamera(structure, cameraIndex, nodeDef) { const cameraDef = structure.getJson().cameras[cameraIndex]; const params = cameraDef[cameraDef.type]; let camera; if (cameraDef.type === "perspective") { camera = new PerspectiveCamera( MathUtils.radToDeg(params.yfov), params.aspectRatio || 1, params.znear || 1, params.zfar || 2e6 ); } else if (cameraDef.type === "orthographic") { camera = new OrthographicCamera( params.xmag / -2, params.xmag / 2, params.ymag / 2, params.ymag / -2, params.znear, params.zfar ); } if (nodeDef.matrix) { camera.matrix.fromArray(nodeDef.matrix); camera.matrix.decompose(camera.position, camera.quaternion, camera.scale); } else { if (nodeDef.translation) { camera.position.fromArray(nodeDef.translation); } if (nodeDef.rotation) { camera.quaternion.fromArray(nodeDef.rotation); } if (nodeDef.scale) { camera.scale.fromArray(nodeDef.scale); } } return camera; } clearNodesToLoad() { this.nodesToLoad = []; } removeOptimization() { this.originalObjects.forEach((obj) => (obj.visible = true)); const disposeMerged = (obj) => { if (obj.parent) { obj.parent.remove(obj); } if (obj.geometry) { obj.geometry.dispose(); } }; if (this.structureGroups) { for (const group of this.structureGroups.values()) { group.meshes.forEach(disposeMerged); group.lines.forEach(disposeMerged); group.lineSegments.forEach(disposeMerged); group.meshes.clear(); group.lines.clear(); group.lineSegments.clear(); } } this.optimizedOriginalMap.clear(); this.mergedMesh.clear(); this.mergedLines.clear(); this.mergedLineSegments.clear(); this.originalObjects.clear(); this.originalObjectsToSelection.clear(); } initializeObjectVisibility() { if (this.maxObjectId > 0) { this.objectVisibility = new Float32Array(this.maxObjectId); for (let i = 0; i < this.maxObjectId; i++) { this.objectVisibility[i] = 1.0; } } } createVisibilityMaterial(material) { material.onBeforeCompile = (shader) => { shader.vertexShader = shader.vertexShader.replace( "#include <common>", ` #include <common> attribute float visibility; varying float vVisibility; ` ); shader.fragmentShader = shader.fragmentShader.replace( "#include <common>", ` #include <common> varying float vVisibility; ` ); shader.vertexShader = shader.vertexShader.replace( "void main() {", ` void main() { vVisibility = visibility; ` ); shader.fragmentShader = shader.fragmentShader.replace( "void main() {", ` void main() { if (vVisibility < 0.5) discard; ` ); }; material.needsUpdate = true; return material; } clear() { this.chunkQueue = []; this.structures.forEach((structure) => { if (structure) { structure.clear(); } }); this.structures = []; this.nodes.forEach((node) => { if (node.object) { if (node.object.parent) { node.object.parent.remove(node.object); } if (node.object.geometry) { node.object.geometry.dispose(); } if (node.object.material) { if (Array.isArray(node.object.material)) { node.object.material.forEach((material) => material.dispose()); } else { node.object.material.dispose(); } } } }); this.nodes.clear(); this.loadedMeshes.forEach((mesh) => { if (mesh.geometry) mesh.geometry.dispose(); if (mesh.material) { if (Array.isArray(mesh.material)) { mesh.material.forEach((material) => material.dispose()); } else { mesh.material.dispose(); } } }); this.loadedMeshes.clear(); this.structureRoots.forEach((rootGroup) => { if (rootGroup) { rootGroup.traverse((child) => { if (child.geometry) child.geometry.dispose(); if (child.material) { if (Array.isArray(child.material)) { child.material.forEach((material) => material.dispose()); } else { child.material.dispose(); } } }); if (rootGroup.parent) { rootGroup.parent.remove(rootGroup); } } }); this.structureRoots.clear(); this.mergedMesh.forEach((mesh) => { if (mesh.geometry) mesh.geometry.dispose(); if (mesh.material) { if (Array.isArray(mesh.material)) { mesh.material.forEach((material) => material.dispose()); } else { mesh.material.dispose(); } } if (mesh.parent) mesh.parent.remove(mesh); }); this.mergedMesh.clear(); this.mergedLines.forEach((line) => { if (line.geometry) line.geometry.dispose(); if (line.material) line.material.dispose(); if (line.parent) line.parent.remove(line); }); this.mergedLines.clear(); this.mergedLineSegments.forEach((lineSegment) => { if (lineSegment.geometry) lineSegment.geometry.dispose(); if (lineSegment.material) lineSegment.material.dispose(); if (lineSegment.parent) lineSegment.parent.remove(lineSegment); }); this.mergedLineSegments.clear(); this.mergedPoints.forEach((points) => { if (points.geometry) points.geometry.dispose(); if (points.material) points.material.dispose(); if (points.parent) points.parent.remove(points); }); this.mergedPoints.clear(); this.geometryCache.clear(); this.materialCache.clear(); this.textureCache.clear(); this.loadedMaterials.clear(); this.nodesToLoad = []; this.handleToObjects.clear(); this.originalObjects.clear(); this.originalObjectsToSelection.clear(); this.optimizedOriginalMap.clear(); this.handleToOptimizedObjects.clear(); this.hiddenHandles.clear(); this.newOptimizedObjects.clear(); this.oldOptimizeObjects.clear(); this.isolatedObjects = []; this.objectTransforms.clear(); this.transformedGeometries.clear(); this.totalLoadedObjects = 0; this.currentMemoryUsage = 0; this.loadedGeometrySize = 0; this.abortController = new AbortController(); this.updateMemoryIndicator(); this.objectIdToIndex.clear(); this.maxObjectId = 0; this.objectVisibility = new Float32Array(); } setStructureTransform(structureId, matrix) { const rootGroup = this.structureRoots.get(structureId); if (rootGroup) { rootGroup.matrix.copy(matrix); rootGroup.matrix.decompose(rootGroup.position, rootGroup.quaternion, rootGroup.scale); return true; } return false; } getStructureRootGroup(structureId) { return this.structureRoots.get(structureId); } addEventListener(event, handler) { if (this.eventHandlers[event]) { this.eventHandlers[event].push(handler); } } removeEventListener(event, handler) { if (this.eventHandlers[event]) { this.eventHandlers[event] = this.eventHandlers[event].filter((h) => h !== handler); } } dispatchEvent(event, data) { if (this.eventHandlers[event]) { this.eventHandlers[event].forEach((handler) => handler(data)); } } registerObjectWithHandle(object, handle) { if (!handle) return; const fullHandle = object.userData.handle; if (!this.handleToObjects.has(fullHandle)) { this.handleToObjects.set(fullHandle, new Set()); } this.handleToObjects.get(fullHandle).add(object); object.userData.structureId = object.userData.handle.split(STRUCTURE_ID_SEPARATOR)[0]; } getObjectsByHandle(handle) { if (!handle) return []; return Array.from(this.handleToObjects.get(handle) || []); } getHandlesByObjects(objects) { if (!objects.length) return []; const handles = new Set(); objects.forEach((obj) => { if (this.originalObjects.has(obj)) handles.add(obj.userData.handle); }); return Array.from(handles); } getMaterialId(material, index) { const props = { type: material.type, color: material.color?.getHex(), map: material.map?.uuid, transparent: material.transparent, opacity: material.opacity, side: material.side, index: index ? 1 : 0, }; return JSON.stringify(props); } addToMaterialGroup(object, groupsMap, optimizeGroupList) { const VERTEX_LIMIT = 100_000; const INDEX_LIMIT = 100_000; const objectGeometryVertexCount = object.geometry.attributes.position.count; const objectGeometryIndexCount = object.geometry.index ? object.geometry.index.count : 0; const material = object.material; let materialId = this.getMaterialId(material, object.geometry.index !== null); let group; if (!groupsMap.has(materialId)) { group = { material, objects: [object], totalVertices: objectGeometryVertexCount, totalIndices: objectGeometryIndexCount, }; groupsMap.set(materialId, group); optimizeGroupList.push(group); } else { group = groupsMap.get(materialId); if ( group.totalVertices + objectGeometryVertexCount > VERTEX_LIMIT || group.totalIndices + objectGeometryIndexCount > INDEX_LIMIT ) { const newGroup = { material, objects: [object], totalVertices: objectGeometryVertexCount, totalIndices: objectGeometryIndexCount, }; materialId = this.getMaterialId(material, object.geometry.index !== null); groupsMap.set(materialId, newGroup); optimizeGroupList.push(newGroup); } else { group.objects.push(object); group.totalVertices += objectGeometryVertexCount; group.totalIndices += objectGeometryIndexCount; } } this.originalObjects.add(object); } yieldToUI() { return new Promise((resolve) => { setTimeout(resolve, 0); }); } async optimizeScene() { console.log("Starting scene optimization..."); this.dispatchEvent("optimizationprogress", { phase: "start", progress: 0, message: "Starting optimization...", }); this.originalObjects.clear(); this.originalObjectsToSelection.clear(); const structureGroups = new Map(); this.dispatchEvent("optimizationprogress", { phase: "collecting", progress: 5, message: "Collecting scene objects...", }); this.scene.traverse((object) => { if (object.userData.structureId) { const structureId = object.userData.structureId; if (!structureGroups.has(structureId)) { structureGroups.set(structureId, { mapMeshes: new Map(), mapLines: new Map(), mapLineSegments: new Map(), mapPoints: new Map(), meshes: [], lines: [], lineSegments: [], points: [], rootGroup: this.structureRoots.get(structureId), }); } const group = structureGroups.get(structureId); if (object instanceof Mesh) { this.addToMaterialGroup(object, group.mapMeshes, group.meshes); } else if (object instanceof LineSegments) { this.addToMaterialGroup(object, group.mapLineSegments, group.lineSegments); } else if (object instanceof Line) { this.addToMaterialGroup(object, group.mapLines, group.lines); } else if (object instanceof Points) { this.addToMaterialGroup(object, group.mapPoints, group.points); } } }); let processedGroups = 0; const totalGroups = structureGroups.size; this.dispatchEvent("optimizationprogress", { phase: "merging", progress: 10, message: `Merging ${totalGroups} structure groups...`, current: 0, total: totalGroups, }); for (const group of structureGroups.values()) { group.mapMeshes.clear(); group.mapLines.clear(); group.mapLineSegments.clear(); group.mapPoints.clear(); await this.mergeMeshGroups(group.meshes, group.rootGroup); await this.yieldToUI(); await this.mergeLineGroups(group.lines, group.rootGroup); await this.yieldToUI(); await this.mergeLineSegmentGroups(group.lineSegments, group.rootGroup); await this.yieldToUI(); await this.mergePointsGroups(group.points, group.rootGroup); processedGroups++; const progress = 10 + Math.round((processedGroups / totalGroups) * 80); this.dispatchEvent("optimizationprogress", { phase: "merging", progress, message: `Processing structure ${processedGroups}/${totalGroups}...`, current: processedGroups, total: totalGroups, }); console.log(`Optimization progress: ${processedGroups}/${totalGroups} structure groups processed (${progress}%)`); await this.yieldToUI(); } this.dispatchEvent("optimizationprogress", { phase: "finalizing", progress: 95, message: "Finalizing optimization...", }); this.originalObjects.forEach((obj) => { obj.visible = false; if (!(obj instanceof Points) && !obj.userData.isEdge) { this.originalObjectsToSelection.add(obj); } }); this.initializeObjectVisibility(); console.log(`Optimization complete. Total objects: ${this.maxObjectId}`); this.dispatchEvent("optimizationprogress", { phase: "complete", progress: 100, message: `Optimization complete! ${this.maxObjectId} objects processed.`, }); this.dispatchEvent("update"); } async mergeMeshGroups(materialGroups, rootGroup) { let processedGroups = 0; for (const group of materialGroups) { if (!group.material) { console.warn("Skipping mesh group with null material"); continue; } try { const geometries = []; const handles = new Set(); const optimizedObjects = []; const objectMapping = new Map(); let currentVertexOffset = 0; for (const mesh of group.objects) { const geometry = mesh.geometry.clone(); const handle = mesh.userData.handle; if (!this.objectIdToIndex.has(handle)) { this.objectIdToIndex.set(handle, this.maxObjectId++); } const objectId = this.objectIdToIndex.get(handle); const vertexCount = geometry.attributes.position.count; const objectIds = new Float32Array(vertexCount); for (let i = 0; i < vertexCount; i++) { objectIds[i] = objectId; } geometry.setAttribute("objectId", new BufferAttribute(objectIds, 1)); objectMapping.set(mesh, { geometry, startVertexIndex: currentVertexOffset, vertexCount: geometry.attributes.position.count, }); currentVertexOffset += geometry.attributes.position.count; geometries.push(geometry); optimizedObjects.push(mesh); handles.add(mesh.userData.handle); } const mergedObjects = []; if (geometries.length > 0) { const mergedGeometry = mergeGeometries(geometries); // Create visibility attribute const totalVertices = mergedGeometry.attributes.position.count; const visibilityArray = new Float32Array(totalVertices); // Initialize all vertices as visible (1.0) for (let i = 0; i < totalVertices; i++) { visibilityArray[i] = 1.0; } // Add visibility attribute to geometry mergedGeometry.setAttribute("visibility", new BufferAttribute(visibilityArray, 1)); if (this.useVAO) { this.createVAO(mergedGeometry); } // Create visibility material const visibilityMaterial = this.createVisibilityMaterial(group.material); const mergedMesh = new Mesh(mergedGeometry, visibilityMaterial); mergedMesh.userData.isOptimized = true; rootGroup.add(mergedMesh); this.mergedMesh.add(mergedMesh); this.optimizedOriginalMap.set(mergedMesh, optimizedObjects); // Store object mappings with visibility tracking this.mergedObjectMap.set(mergedMesh.uuid, { objectMapping,