@inweb/viewer-three
Version:
JavaScript library for rendering CAD and BIM files in a browser using Three.js
1,616 lines (1,371 loc) • 55.3 kB
JavaScript
import {
BufferGeometry,
PointsMaterial,
Points,
Mesh,
TriangleStripDrawMode,
TriangleFanDrawMode,
LineSegments,
Line,
LineLoop,
Group,
Vector3,
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";
export class DynamicGltfLoader {
constructor(camera, scene, renderer) {
this.camera = camera;
this.scene = scene;
this.renderer = renderer;
this.eventHandlers = {
geometryprogress: [],
databasechunk: [],
geometryend: [],
geometryerror: [],
update: [],
geometrymemory: [],
};
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.lastUpdateTime = 0;
this.updateInterval = 1000;
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();
this.maxConcurrentChunks = 8;
this.activeChunkLoads = 0;
this.chunkQueue = [];
}
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`);
}
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 = `${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) {
if (error.name !== "AbortError") {
console.error(`Error loading node ${nodeId}:`, error);
}
node.loading = false;
}
}
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,
})),
});
}
async processNodeHierarchy(structure, nodeId, parentGroup) {
const nodeDef = structure.json.nodes[nodeId];
let nodeGroup = null;
let handle = null;
if (nodeDef.extras?.handle) {
handle = `${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 = `${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 = `${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,
});
}
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,
});
const currentTime = Date.now();
if (currentTime - this.lastUpdateTime >= this.updateInterval) {
this.dispatchEvent("update");
this.lastUpdateTime = currentTime;
}
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();
if (node.group && node.group.matrix) {
transformedBox.applyMatrix4(node.group.matrix);
if (node.group.parent && node.group.parent.matrix) {
transformedBox.applyMatrix4(node.group.parent.matrix);
}
}
totalExtent.union(transformedBox);
}
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();
}
clear() {
this.chunkQueue = [];
this.structures.forEach((structure) => {
if (structure) {
structure.clear();
}
});
this.structures = [];
// Clear all nodes and unload their objects
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();
// Clear all loaded meshes
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();
// Clear all caches
this.geometryCache.clear();
this.materialCache.clear();
this.textureCache.clear();
this.loadedMaterials.clear();
// Clear all maps and sets
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 = [];
// Reset counters and state
this.totalLoadedObjects = 0;
this.lastUpdateTime = 0;
this.currentMemoryUsage = 0;
this.loadedGeometrySize = 0;
this.abortController = new AbortController();
this.updateMemoryIndicator();
}
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("_")[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);
}
optimizeScene() {
this.originalObjects.clear();
this.originalObjectsToSelection.clear();
const structureGroups = new Map();
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);
}
}
});
for (const group of structureGroups.values()) {
group.mapMeshes.clear();
group.mapLines.clear();
group.mapLineSegments.clear();
group.mapPoints.clear();
this.mergeMeshGroups(group.meshes, group.rootGroup);
this.mergeLineGroups(group.lines, group.rootGroup);
this.mergeLineSegmentGroups(group.lineSegments, group.rootGroup);
this.mergePointsGroups(group.points, group.rootGroup);
}
this.originalObjects.forEach((obj) => {
obj.visible = false;
if (!(obj instanceof Points) && !obj.userData.isEdge) {
this.originalObjectsToSelection.add(obj);
}
});
this.dispatchEvent("update");
}
mergeMeshGroups(materialGroups, rootGroup) {
for (const group of materialGroups) {
try {
const geometries = [];
const handles = new Set();
const optimizedObjects = [];
for (const mesh of group.objects) {
const geometry = mesh.geometry.clone();
mesh.updateWorldMatrix(true, false);
geometry.applyMatrix4(mesh.matrixWorld);
geometries.push(geometry);
optimizedObjects.push(mesh);
handles.add(mesh.userData.handle);
}
const mergedObjects = [];
if (geometries.length > 0) {
const mergedGeometry = mergeGeometries(geometries);
if (this.useVAO) {
this.createVAO(mergedGeometry);
}
const mergedMesh = new Mesh(mergedGeometry, group.material);
rootGroup.add(mergedMesh);
this.mergedMesh.add(mergedMesh);
this.optimizedOriginalMap.set(mergedMesh, optimizedObjects);
mergedObjects.push(mergedMesh);
geometries.forEach((geometry) => {
geometry.dispose();
});
}
handles.forEach((handle) => {
if (this.handleToOptimizedObjects.has(handle)) {
const existingObjects = this.handleToOptimizedObjects.get(handle);
existingObjects.push(...mergedObjects);
this.handleToOptimizedObjects.set(handle, existingObjects);
} else {
this.handleToOptimizedObjects.set(handle, mergedObjects);
}
});
} catch (error) {
console.error("Failed to merge meshes for material:", error);
group.objects.forEach((mesh) => {
mesh.visible = true;
});
}
}
}
mergeLineGroups(materialGroups, rootGroup) {
for (const group of materialGroups) {
if (group.objects.length === 0) continue;
const handles = new Set();
let totalVertices = 0;
group.objects.map((line) => {
handles.add(line.userData.handle);
totalVertices += line.geometry.attributes.position.count;
});
const positions = new Float32Array(totalVertices * 3);
let posOffset = 0;
const indices = [];
let vertexOffset = 0;
group.objects.forEach((line) => {
const geometry = line.geometry;
const positionAttr = geometry.attributes.position;
const vertexCount = positionAttr.count;
line.updateWorldMatrix(true, false);
const matrix = line.matrixWorld;
const vector = new Vector3();
for (let i = 0; i < vertexCount; i++) {
vector.fromBufferAttribute(positionAttr, i);
vector.applyMatrix4(matrix);
positions[posOffset++] = vector.x;
positions[posOffset++] = vector.y;
positions[posOffset++] = vector.z;
}
for (let i = 0; i < vertexCount - 1; i++) {
indices.push(vertexOffset + i, vertexOffset + i + 1);
}
vertexOffset += vertexCount;
});
const geometry = new BufferGeometry();
geometry.setAttribute("position", new BufferAttribute(positions, 3));
geometry.setIndex(indices);
geometry.computeBoundingSphere();
geometry.computeBoundingBox();
const mergedLine = new LineSegments(geometry, group.material);
const mergedObjects = [mergedLine];
if (this.useVAO) {
this.createVAO(mergedLine);
}
rootGroup.add(mergedLine);
this.mergedLines.add(mergedLine);
this.optimizedOriginalMap.set(mergedLine, group.objects);
handles.forEach((handle) => {
if (this.handleToOptimizedObjects.has(handle)) {
const existingObjects = this.handleToOptimizedObjects.get(handle);
existingObjects.push(...mergedObjects);
this.handleToOptimizedObjects.set(handle, existingObjects);
} else {
this.handleToOptimizedObjects.set(handle, mergedObjects);
}
});
}
}
mergeLineSegmentGroups(materialGroups, rootGroup) {
for (const group of materialGroups) {
try {
const geometries = [];
const optimizedObjects = [];
const handles = new Set();
for (const line of group.objects) {
const geometry = line.geometry.clone();
line.updateWorldMatrix(true, false);
geometry.applyMatrix4(line.matrixWorld);
geometries.push(geometry);
optimizedObjects.push(line);
handles.add(line.userData.handle);
}
const mergedObjects = [];
if (geometries.length > 0) {
const mergedGeometry = mergeGeometries(geometries, false);
const mergedLine = new LineSegments(mergedGeometry, group.material);
if (this.useVAO) {
this.createVAO(mergedLine);
}
rootGroup.add(mergedLine);
this.mergedLineSegments.add(mergedLine);
this.optimizedOriginalMap.set(mergedLine, optimizedObjects);
mergedObjects.push(mergedLine);
geometries.forEach((geometry) => {
geometry.dispose();
});
}
handles.forEach((handle) => {
if (this.handleToOptimizedObjects.has(handle)) {
const existingObjects = this.handleToOptimizedObjects.get(handle);
existingObjects.push(...mergedObjects);
this.handleToOptimizedObjects.set(handle, existingObjects);
} else {
this.handleToOptimizedObjects.set(handle, mergedObjects);
}
});
} catch (error) {
console.warn("Failed to merge line segments for material:", error);
group.objects.forEach((line) => {
line.visible = true;
});
}
}
}
mergePointsGroups(materialGroups, rootGroup) {
for (const group of materialGroups) {
try {
const geometries = [];
const optimizedObjects = [];
const handles = new Set();
for (const points of group.objects) {
const geometry = points.geometry.clone();
points.updateWorldMatrix(true, false);
geometry.applyMatrix4(points.matrixWorld);
geometries.push(geometry);
optimizedObjects.push(points);
handles.add(points.userData.handle);
}
const mergedObjects = [];
if (geometries.length > 0) {
const mergedGeometry = mergeGeometries(geometries, false);
const mergedPoints = new Points(mergedGeometry, group.material);
if (this.useVAO) {
this.createVAO(mergedPoints);
}
rootGroup.add(mergedPoints);
this.mergedPoints.add(mergedPoints);
this.optimizedOriginalMap.set(mergedPoints, optimizedObjects);
mergedObjects.push(mergedPoints);
geometries.forEach((geometry) => {
geometry.dispose();
});
}
handles.forEach((handle) => {
if (this.handleToOptimizedObjects.has(handle)) {
const existingObjects = this.handleToOptimizedObjects.get(handle);
existingObjects.push(...mergedObjects);
this.handleToOptimizedObjects.set(handle, existingObjects);
} else {
this.handleToOptimizedObjects.set(handle, mergedObjects);
}
});
} catch (error) {
console.warn("Failed to merge points for material:", error);
group.objects.forEach((points) => {
points.visible = true;
});
}
}
}
mergeInSingleSegment(structureId, rootGroup) {
const lineSegmentsArray = [...this.mergedLineSegments, ...this.mergedLines].filter(
(obj) => obj.userData.structureId === structureId
);
if (lineSegmentsArray.length === 0) return;
try {
const geometriesWithIndex = [];
const hasNormals = lineSegmentsArray.some((segment) => segment.geometry.attributes.normal !== undefined);
lineSegmentsArray.forEach((segment) => {
const clonedGeometry = segment.geometry.clone();
segment.updateWorldMatrix(true, false);
clonedGeometry.applyMatrix4(segment.matrixWorld);
if (hasNormals && !clonedGeometry.attributes.normal) {
clonedGeometry.computeVertexNormals();
}
if (!hasNormals && clonedGeometry.attributes.normal) {
clonedGeometry.deleteAttribute("normal");
}
const colorArray = new Float32Array(clonedGeometry.attributes.position.count * 3);
for (let i = 0; i < colorArray.length; i += 3) {
colorArray[i] = segment.material.color.r;
colorArray[i + 1] = segment.material.color.g;
colorArray[i + 2] = segment.material.color.b;
}
clonedGeometry.setAttribute("color", new BufferAttribute(colorArray, 3));
if (!clonedGeometry.index) {
const indices = [];
const posCount = clonedGeometry.attributes.position.count;
for (let i = 0; i < posCount - 1; i += 2) {
indices.push(i, i + 1);
}
clonedGeometry.setIndex(indices);
}
geometriesWithIndex.push(clonedGeometry);
});
const finalGeometry = mergeGeometries(geometriesWithIndex, false);
const material = new LineBasicMaterial({
vertexColors: true,
});
if (this.useVAO) {
this.createVAO(finalGeometry);
}
const mergedLine = new LineSegments(finalGeometry, material);
mergedLine.userData.structureId = structureId;
rootGroup.add(mergedLine);
this.mergedLineSegments.add(mergedLine);
lineSegmentsArray.forEach((obj) => {
if (obj.parent) {
obj.parent.remove(obj);
}
obj.geometry.dispose();
});
} catch (error) {
console.error("Failed to merge geometries:", error);
lineSegmentsArray.forEach((obj) => {
obj.visible = true;
rootGroup.add(obj);
});
}
}
showOriginalObjects(objects) {
objects.forEach((obj) => {
if (this.originalObjects.has(obj)) {
obj.visible = true;
}
});
}
hideOriginalObjects(objects) {
objects.forEach((obj) => {
if (this.originalObjects.has(obj)) {
obj.visible = false;
}
});
}
createVAO(geometry) {
if (!this.useVAO) {
return;
}
if (geometry.attributes?.position?.count < 1000) {
return;
}
const gl = this.renderer.getContext();
const vao = gl.createVertexArray();
gl.bindVertexArray(vao);
for (const name in geometry.attributes) {
const attribute = geometry.attributes[name];
const buffer = this.renderer.properties.get(attribute).buffer;
gl.bindBuffer(gl.ARRAY_BUFFER, buffer);
gl.enableVertexAttribArray(attribute.itemSize);
gl.vertexAttribPointer(attribute.itemSize, attribute.itemSize, gl.FLOAT, false, 0, 0);
}
if (geometry.index) {
const indexBuffer = this.renderer.properties.get(geometry.index).buffer;
gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, indexBuffer);
}
gl.bindVertexArray(null);
gl.bindBuffer(gl.ARRAY_BUFFER, null);
gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, null);
geometry.vao = vao;
}
getOriginalObjectForSelect() {
const optimizedOriginals = [];
for (const obj of this.originalObjectsToSelection) {
if (this.hiddenHandles.has(obj.userData.handle)) {
continue;
}
optimizedOriginals.push(obj);
}
return optimizedOriginals;
}
isolateObjects(handles) {
if (this.hiddenHandles.size !== 0) {
this.hiddenHandles.clear();
this.syncHiddenObjects();
}
for (const handle of this.handleToOptimizedObjects.keys()) {
if (!handles.has(handle)) {
this.hiddenHandles.add(handle);
}
}
this.syncHiddenObjects();
}
showAllHiddenObjects() {
thi