/////////////////////////////////////////////////////////////////////////////// // Copyright (C) 2002-2023, 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-2021 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 * as ODA from "open-cloud-client"; import * as THREE from "three"; import { GLTF, GLTFLoader } from "three/examples/jsm/loaders/GLTFLoader.js"; import { GLTFLoadingManager } from "./loaders/GLTFLoadingManager"; import { PanDragger } from "./draggers/PanDragger"; import { OrbitDragger } from "./draggers/OrbitDragger"; import { ZoomDragger } from "./draggers/ZoomDragger"; import { WalkDragger } from "./draggers/WalkDragger"; import { ClippingPlaneDragger } from "./draggers/ClippingPlaneDragger"; import { IComponent } from "./IComponent"; import { LightComponent } from "./components/LightComponent"; import { BackgroundComponent } from "./components/BackgroundComponent"; import { DefaultCameraPositionComponent } from "./components/DefaultCameraPositionComponent"; import { ResizeCanvasComponent } from "./components/ResizeCanvasComponent"; import { RenderLoopComponent } from "./components/RenderLoopComponent"; // import { ObjectSelectionComponent } from "./components/ObjectSelectionComponent"; if (typeof ODA.EventEmitter2 === "undefined") { console.error(new TypeError("Three.js Viewer require Open Cloud Client (https://cloud.opendesign.com/docs)")); } export class ThreejsViewer extends ODA.EventEmitter2 implements ODA.IViewer { public client: ODA.Client | undefined; private _options: ODA.Options; private clientoptionschange: (event: any) => void; private canvaseventlistener: (event: any) => void; public canvas: HTMLCanvasElement | undefined; public canvasEvents: string[]; public scene: THREE.Scene | undefined; public camera: THREE.PerspectiveCamera | undefined; public renderer: THREE.WebGLRenderer | undefined; public models: Array; public selectedObjects: Array; private draggerFactory: Record; private _activeDragger: any; private components: Array; private renderNeeded: boolean; private renderTime: DOMHighResTimeStamp; constructor(client?: ODA.Client) { super(); this._options = new ODA.Options(this); this.client = client; this.clientoptionschange = (event: ODA.OptionsChangeEvent) => (this._options.data = event.data.data); this.canvasEvents = ODA.CANVAS_EVENTS; this.canvaseventlistener = (event: Event) => this.emit(event); this.draggerFactory = { Pan: PanDragger, Zoom: ZoomDragger, Orbit: OrbitDragger, Walk: WalkDragger, Clipping: ClippingPlaneDragger, }; this._activeDragger = null; this.models = []; this.components = []; this.selectedObjects = []; this.renderTime = 0; this.render = this.render.bind(this); this.update = this.update.bind(this); } get options(): ODA.Options { return this._options; } get draggers(): string[] { return Object.keys(this.draggerFactory); } initialize(canvas: HTMLCanvasElement, onProgress?: (event: ProgressEvent) => void): Promise { if (this.client) { this.client.addEventListener("optionschange", this.clientoptionschange); this.options.data = this.client.options.data; } this.addEventListener("optionschange", (event) => this.syncOptions(event.data)); this.scene = new THREE.Scene(); const rect = canvas.parentElement.getBoundingClientRect(); const width = rect.width || 1; const height = rect.height || 1; this.camera = new THREE.PerspectiveCamera(45, width / height, 0.01, 1000); this.camera.up.set(0, 0, 1); this.renderer = new THREE.WebGLRenderer({ canvas, antialias: true }); this.renderer.setPixelRatio(window.devicePixelRatio); this.renderer.setSize(width, height); this.renderer.toneMapping = THREE.LinearToneMapping; this.canvas = canvas; this.canvasEvents.forEach((x) => canvas.addEventListener(x, this.canvaseventlistener)); this.components.push(new LightComponent(this)); this.components.push(new BackgroundComponent(this)); this.components.push(new DefaultCameraPositionComponent(this)); this.components.push(new ResizeCanvasComponent(this)); this.components.push(new RenderLoopComponent(this as any)); // this.components.push(new ObjectSelectionComponent(this as any)); this.options.notifierChangeEvent(); this.renderTime = performance.now(); this.render(this.renderTime); if (typeof onProgress === "function") onProgress(new ProgressEvent("progress", { lengthComputable: true, loaded: 1, total: 1 })); return Promise.resolve(this); } dispose(): this { this.cancel(); this.emitEvent({ type: "dispose" }); this.components.forEach((component: IComponent) => component.dispose()); this.components = []; this.setActiveDragger(""); this.removeAllListeners(); if (this.canvas) { this.canvasEvents.forEach((x) => this.canvas.removeEventListener(x, this.canvaseventlistener)); this.canvas = undefined; } if (this.renderer) this.renderer.dispose(); this.renderer = undefined; this.camera = undefined; this.scene = undefined; if (this.client) this.client.removeEventListener("optionschange", this.clientoptionschange); return this; } isInitialized(): boolean { return !!this.renderer; } public render(time: DOMHighResTimeStamp): void { if (!this.renderNeeded) return; if (!this.renderer) return; this.renderer.render(this.scene, this.camera); this.renderNeeded = false; const deltaTime = (time - this.renderTime) / 1000; this.renderTime = time; this.emitEvent({ type: "render", time, deltaTime }); } public update(force = false): void { this.renderNeeded = true; if (force) this.render(performance.now()); this.emitEvent({ type: "update", data: force }); } public syncOptions(options: ODA.OptionsData = this.options.data): void { // this.update(); } loadReferences(model: ODA.Model | ODA.File | ODA.Assembly): Promise { // todo: load reference as text fonts return Promise.resolve(this); } async open(model: ODA.Model | ODA.File | ODA.Assembly): Promise { if (!this.renderer) return this; this.cancel(); this.clear(); this.emitEvent({ type: "open", model }); if (model instanceof ODA.File || model instanceof ODA.Assembly) { const models = (await model.getModels()) || []; model = models.find((model) => model.default) || models[0]; } if (!model) throw new Error("No default model found"); const geometryType = model.database.split(".").pop(); if (geometryType !== "gltf") throw new Error(`Unknown geometry type: ${geometryType}`); const url = `${model.httpClient.serverUrl}${model.path}/${model.database}`; const params = { requestHeader: model.httpClient.headers }; await this.loadReferences(model); await this.loadGltfFile(url, undefined, params); return this; } cancel(): this { this.emitEvent({ type: "cancel" }); return this; } openGltfFile( file: string | ArrayBuffer | Blob, externalData: Map = new Map(), params: { path?: string; requestHeader?: HeadersInit; crossOrigin?: string; withCredentials?: boolean; } = {} ): Promise { if (!this.renderer) return Promise.resolve(this); this.cancel(); this.clear(); this.emitEvent({ type: "open" }); return this.loadGltfFile(file, externalData, params); } async loadGltfFile( file: string | ArrayBuffer | Blob, externalData: Map = new Map(), params: { path?: string; requestHeader?: HeadersInit; crossOrigin?: string; withCredentials?: boolean; } = {} ): Promise { const manager = new GLTFLoadingManager(file, externalData, params); try { this.emitEvent({ type: "geometrystart" }); const loader = new GLTFLoader(manager); loader.setPath(manager.path); loader.setRequestHeader(params.requestHeader as any); loader.setCrossOrigin(params.crossOrigin || loader.crossOrigin); loader.setWithCredentials(params.withCredentials || loader.withCredentials); const gltf = await loader.loadAsync(manager.fileURL, (event: ProgressEvent) => { const { lengthComputable, loaded, total } = event; const progress = lengthComputable ? loaded / total : 1; this.emitEvent({ type: "geometryprogress", data: progress }); }); if (!this.scene) return this; if (!gltf.scene) throw new Error("No glTF scene found"); this.models.push(gltf); this.scene.add(gltf.scene); this.update(); this.emitEvent({ type: "databasechunk" }); this.emitEvent({ type: "geometryend", data: gltf.scene }); } catch (error) { this.emitEvent({ type: "geometryerror", data: error }); throw error; } finally { manager.dispose(); } return this; } clear(): this { function disposeMaterial(material: any) { const materials = Array.isArray(material) ? material : [material]; materials.forEach((material: any) => { // Object.keys(material).forEach((key) => material[key]?.dispose?.()); material.dispose(); }); } function disposeObject(object: any) { if (object.geometry) object.geometry.dispose(); if (object.material) disposeMaterial(object.material); } this.selectedObjects = []; this.models.forEach((gltf) => gltf.scene.traverse(disposeObject)); this.models.forEach((gltf) => gltf.scene.removeFromParent()); this.models = []; this.update(); this.emitEvent({ type: "clear" }); return this; } activeDragger(): any | null { return this._activeDragger; } setActiveDragger(name: string): any { if (!this._activeDragger || this._activeDragger.name !== name) { if (this._activeDragger) { this._activeDragger.dispose(); this._activeDragger = null; } const Constructor = this.draggerFactory[name]; if (Constructor) { this._activeDragger = new Constructor(this); this._activeDragger.name = name; } } return this._activeDragger; } resetActiveDragger(): void { const dragger = this._activeDragger; if (dragger) { this.setActiveDragger(""); this.setActiveDragger(dragger.name); } } is3D(): boolean { return false; } executeCommand(id: string, ...args: any[]): any { return ODA.commands("ThreeJS").executeCommand(id, this, ...args); } }