UNPKG

@inweb/viewer-three

Version:

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

454 lines (360 loc) 14.5 kB
/////////////////////////////////////////////////////////////////////////////// // Copyright (C) 2002-2025, Open Design Alliance (the "Alliance"). // All rights reserved. // // This software and its documentation and related materials are owned by // the Alliance. The software may only be incorporated into application // programs owned by members of the Alliance, subject to a signed // Membership Agreement and Supplemental Software License Agreement with the // Alliance. The structure and organization of this software are the valuable // trade secrets of the Alliance and its suppliers. The software is also // protected by copyright law and international treaty provisions. Application // programs incorporating this software must include the following statement // with their copyright notices: // // This application incorporates Open Design Alliance software pursuant to a // license agreement with Open Design Alliance. // Open Design Alliance Copyright (C) 2002-2025 by Open Design Alliance. // All rights reserved. // // By use of this software, its documentation or related materials, you // acknowledge and accept the above terms. /////////////////////////////////////////////////////////////////////////////// import { Box3, Object3D, Vector3 } from "three"; import { IInfo, Info } from "@inweb/viewer-core"; import { IModelImpl } from "./IModelImpl"; import { convertUnits } from "../measurement/UnitConverter"; import { getDisplayUnit } from "../measurement/UnitFormatter"; export class ModelImpl implements IModelImpl { public id: string; public scene: Object3D; private handleToObjects: Map<any, Set<Object3D>>; private originalObjects: Set<Object3D>; constructor(scene: Object3D) { this.scene = scene; this.handleToObjects = new Map(); this.originalObjects = new Set(); this.scene.traverse((object) => { this.originalObjects.add(object); const handle = object.userData.handle; if (!handle) return; let objects = this.handleToObjects.get(handle); if (!objects) { objects = new Set(); this.handleToObjects.set(handle, objects); } objects.add(object); }); } dispose() { function disposeMaterial(material: any) { // if (material.alphaMap) material.alphaMap.dispose(); // if (material.anisotropyMap) material.anisotropyMap.dispose(); // if (material.aoMap) material.aoMap.dispose(); // if (material.bumpMap) material.bumpMap.dispose(); // if (material.clearcoatMap) material.clearcoatMap.dispose(); // if (material.clearcoatNormalMap) material.clearcoatNormalMap.dispose(); // if (material.clearcoatRoughnessMap) material.clearcoatRoughnessMap.dispose(); // if (material.displacementMap) material.displacementMap.dispose(); // if (material.emissiveMap) material.emissiveMap.dispose(); // if (material.gradientMap) material.gradientMap.dispose(); // if (material.envMap) material.envMap.dispose(); // if (material.iridescenceMap) material.iridescenceMap.dispose(); // if (material.lightMap) material.lightMap.dispose(); // if (material.map) material.map.dispose(); // if (material.metalnessMap) material.metalnessMap.dispose(); // if (material.normalMap) material.normalMap.dispose(); // if (material.roughnessMap) material.roughnessMap.dispose(); // if (material.sheenColorMap) material.sheenColorMap.dispose(); // if (material.sheenRoughnessMap) material.sheenRoughnessMap.dispose(); // if (material.specularMap) material.specularMap.dispose(); // if (material.thicknessMap) material.thicknessMap.dispose(); // if (material.transmissionMap) material.transmissionMap.dispose(); material.dispose(); } function disposeMaterials(material: any) { const materials = Array.isArray(material) ? material : [material]; materials.forEach((material: any) => disposeMaterial(material)); } function disposeObject(object: any) { if (object.geometry) object.geometry.dispose(); if (object.material) disposeMaterials(object.material); } this.handleToObjects = undefined; this.originalObjects = undefined; this.scene.traverse(disposeObject); this.scene.clear(); } getUnits(): string { return "Meters"; } getUnitScale(): number { return convertUnits(this.getUnits(), "Meters", 1); } getUnitString(): string { return getDisplayUnit(this.getUnits()); } getPrecision(): number { return 2; } getInfo(): IInfo { // ===================== AI-CODE-START ====================== // Source: Claude Sonnet 4.5 // Date: 2025-10-02 // Reviewer: roman.mochalov@opendesign.com // Issue: CLOUD-5738 const geometries = new Set(); const materials = new Set(); const textures = new Set(); let totalObjects = 0; let totalTriangles = 0; let totalPoints = 0; let totalLines = 0; let totalEdges = 0; let geometryBytes = 0; let textureBytes = 0; this.scene.traverse((object: any) => { totalObjects++; if (object.geometry) { const geometry = object.geometry; if (!geometries.has(geometry)) { geometries.add(geometry); if (geometry.attributes) { for (const name in geometry.attributes) { const attribute = geometry.attributes[name]; if (attribute && attribute.array) { geometryBytes += attribute.array.byteLength; } } } if (geometry.index && geometry.index.array) { geometryBytes += geometry.index.array.byteLength; } } if (geometry.index) { const indexCount = geometry.index.count; if (object.isLine || object.isLineSegments) { totalLines += indexCount / 2; } else if (object.isPoints) { totalPoints += indexCount; } else { totalTriangles += indexCount / 3; } } else if (geometry.attributes && geometry.attributes.position) { const positionCount = geometry.attributes.position.count; if (object.isLine || object.isLineSegments) { totalLines += positionCount / 2; } else if (object.isPoints) { totalPoints += positionCount; } else { totalTriangles += positionCount / 3; } } if (object.isLineSegments && geometry.attributes.position) { totalEdges += geometry.attributes.position.count / 2; } } if (object.material) { const materialsArray = Array.isArray(object.material) ? object.material : [object.material]; materialsArray.forEach((material: any) => { materials.add(material); if (material.map && !textures.has(material.map)) { textures.add(material.map); textureBytes += estimateTextureSize(material.map); } const textureProps = [ "alphaMap", "aoMap", "bumpMap", "displacementMap", "emissiveMap", "envMap", "lightMap", "metalnessMap", "normalMap", "roughnessMap", "specularMap", "clearcoatMap", "clearcoatNormalMap", "clearcoatRoughnessMap", "iridescenceMap", "sheenColorMap", "sheenRoughnessMap", "thicknessMap", "transmissionMap", "anisotropyMap", "gradientMap", ]; textureProps.forEach((prop) => { const texture = material[prop]; if (texture && !textures.has(texture)) { textures.add(texture); textureBytes += estimateTextureSize(texture); } }); }); } }); function estimateTextureSize(texture: any): number { if (!texture.image) return 0; const width = texture.image.width || 0; const height = texture.image.height || 0; // Estimate bytes per pixel (RGBA = 4 bytes) const bytesPerPixel = 4; // Account for mipmaps (adds ~33% more memory) const mipmapMultiplier = texture.generateMipmaps ? 1.33 : 1; return width * height * bytesPerPixel * mipmapMultiplier; } // ===================== AI-CODE-END ====================== const info = new Info(); info.scene.objects = totalObjects; info.scene.triangles = Math.floor(totalTriangles); info.scene.points = Math.floor(totalPoints); info.scene.lines = Math.floor(totalLines); info.scene.edges = Math.floor(totalEdges); info.memory.geometries = geometries.size; info.memory.geometryBytes = geometryBytes; info.memory.textures = textures.size; info.memory.textureBytes = Math.floor(textureBytes); info.memory.materials = materials.size; info.memory.totalEstimatedGpuBytes = geometryBytes + Math.floor(textureBytes); info.optimizedScene.objects = info.scene.objects; info.optimizedScene.triangles = info.scene.triangles; info.optimizedScene.points = info.scene.points; info.optimizedScene.lines = info.scene.lines; info.optimizedScene.edges = info.scene.edges; return info; } getExtents(target: Box3): Box3 { this.scene.traverseVisible((object) => target.expandByObject(object)); return target; } getObjects(): Object3D[] { return Array.from(this.originalObjects); } getVisibleObjects(): Object3D[] { const objects = []; this.scene.traverseVisible((object) => objects.push(object)); return objects.filter((object) => object.userData.handle); } getObjectsByHandles(handles: string | string[]): Object3D[] { if (!Array.isArray(handles)) handles = [handles]; const ownHandles = []; handles.forEach((handle) => { const index = handle.indexOf(":"); if (index !== -1) { if (handle.slice(0, index) !== this.id) return; handle = handle.slice(index + 1); } ownHandles.push(handle); }); const handlesSet = new Set<string>(ownHandles); const objects = []; handlesSet.forEach((handle) => { objects.push(Array.from(this.handleToObjects.get(handle) || [])); }); return objects.flat(); } getHandlesByObjects(objects: Object3D | Object3D[]): string[] { if (!Array.isArray(objects)) objects = [objects]; const handleSet = new Set<string>(); objects .filter((object) => this.originalObjects.has(object)) .forEach((object) => { handleSet.add(`${this.id}:${object.userData.handle}`); }); return Array.from(handleSet); } hideObjects(objects: Object3D | Object3D[]): this { if (!Array.isArray(objects)) objects = [objects]; objects .filter((object) => this.originalObjects.has(object)) .forEach((object) => { object.visible = false; }); return this; } hideAllObjects(): this { return this.isolateObjects([]); } isolateObjects(objects: Object3D | Object3D[]): this { if (!Array.isArray(objects)) objects = [objects]; const visibleSet = new Set(objects); objects .filter((object) => this.originalObjects.has(object)) .forEach((object) => { object.traverseAncestors((parent) => visibleSet.add(parent)); }); this.scene.traverse((object) => (object.visible = visibleSet.has(object))); return this; } showObjects(objects: Object3D | Object3D[]): this { if (!Array.isArray(objects)) objects = [objects]; objects .filter((object) => this.originalObjects.has(object)) .forEach((object) => { object.visible = true; object.traverseAncestors((parent) => (parent.visible = true)); }); return this; } showAllObjects(): this { this.scene.traverse((object) => (object.visible = true)); return this; } highlightObjects(objects: Object3D | Object3D[]): this { return this; } unhighlightObjects(objects: Object3D | Object3D[]): this { return this; } explode(scale = 0, coeff = 4): this { const centers = new Map(); const getObjectCenter = (object: Object3D, target: Vector3): Vector3 => { const extents = new Box3().setFromObject(object); const handle = object.userData.handle; if (!handle) return extents.getCenter(target); const center = centers.get(handle); if (center) return target.copy(center); const objects = this.getObjectsByHandles(handle); objects.forEach((x: Object3D) => extents.expandByObject(x)); extents.getCenter(target); centers.set(handle, target.clone()); return target; }; function calcExplodeDepth(object: Object3D, depth: number): number { let result = depth; object.children.forEach((x: Object3D) => { const objectDepth = calcExplodeDepth(x, depth + 1); if (result < objectDepth) result = objectDepth; }); object.userData.originalPosition = object.position.clone(); object.userData.originalCenter = getObjectCenter(object, new Vector3()); return result; } const explodeScale = scale / 100; const explodeRoot = this.scene; if (!explodeRoot.userData.explodeDepth) explodeRoot.userData.explodeDepth = calcExplodeDepth(explodeRoot, 1); const maxDepth = explodeRoot.userData.explodeDepth; const scaledExplodeDepth = explodeScale * maxDepth + 1; const explodeDepth = 0 | scaledExplodeDepth; const currentSegmentFraction = scaledExplodeDepth - explodeDepth; function explodeObject(object, depth: number) { if (object.isCamera) return; if (object.userData.isHighlightWireframe) return; object.position.copy(object.userData.originalPosition); if (depth > 0 && depth <= explodeDepth) { let objectScale = explodeScale * coeff; if (depth === explodeDepth) objectScale *= currentSegmentFraction; const parentCenter = object.parent.userData.originalCenter; const objectCenter = object.userData.originalCenter; const objectOffset = objectCenter.clone().sub(parentCenter).multiplyScalar(objectScale); object.position.add(objectOffset); } object.children.forEach((x: Object3D) => explodeObject(x, depth + 1)); } explodeObject(explodeRoot, 0); this.scene.updateMatrixWorld(); return this; } }