@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
text/typescript
///////////////////////////////////////////////////////////////////////////////
// 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;
}
}