UNPKG

@inweb/viewer-visualize

Version:

JavaScript library for rendering CAD and BIM files in a browser using VisualizeJS

1,323 lines (1,077 loc) 42.4 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 { EventEmitter2 } from "@inweb/eventemitter2"; import { Assembly, Client, File, Model } from "@inweb/client"; import { CANVAS_EVENTS, CanvasEventMap, Dragger, FileSource, IClippingPlane, IComponent, IEntity, IDragger, ILoader, IOrthogonalCamera, IOptions, IPoint, IViewer, IViewpoint, Options, OptionsEventMap, ViewerEventMap, } from "@inweb/viewer-core"; import { IMarkup, IWorldTransform } from "@inweb/markup"; import { draggers } from "./Draggers"; import { commands } from "./Commands"; import { components } from "./Components"; import { loaders } from "./Loaders"; import { loadVisualizeJs } from "./utils"; import { MarkupFactory, MarkupType } from "./Markup/MarkupFactory"; const OVERLAY_VIEW_NAME = "$OVERLAY_VIEW_NAME"; const isExist = (value) => value !== undefined && value !== null; /** * 3D viewer powered by {@link https://cloud.opendesign.com/docs/index.html#/visualizejs | VisualizeJS} * library. */ export class Viewer extends EventEmitter2<ViewerEventMap & CanvasEventMap & OptionsEventMap> implements IViewer, IWorldTransform { private _activeDragger: IDragger | null; private _components: Array<IComponent>; private _enableAutoUpdate: boolean; private _isNeedRender: boolean; private _isRunAsyncUpdate: boolean; private _renderTime: DOMHighResTimeStamp; protected _options: Options; protected _visualizeJsUrl = ""; protected _visualizeJs: any; protected _visualizeTimestamp: number; protected _crossOrigin; private canvaseventlistener: (event: Event) => void; public canvasEvents: string[]; private _markup: IMarkup; public canvas: HTMLCanvasElement | undefined; public _abortControllerForReferences: AbortController | undefined; public client: Client | undefined; public loaders: Array<ILoader>; /** * @param client - The `Client` instance that is used to load model reference files from the Open Cloud * Server. Do not specify `Client` if you need a standalone viewer instance to view `VSFX` files from * the web or from local computer. * @param params - An object containing viewer configuration parameters. * @param params.visualizeJsUrl - `VisualizeJS` library URL. Set this URL to use your own library * instance, or specify `undefined` or blank to use the default URL defined by `Viewer.visualize` * library you are using. * @param params.crossOrigin - The * {@link https://developer.mozilla.org/docs/Web/HTML/Attributes/crossorigin | crossorigin} content * attribute on `Visalize.js` script element. One of the following values: `""`, `anonymous` or * `use-credentials`. * @param params.enableAutoUpdate - Enable auto-update of the viewer after any changes. If the * auto-update is disabled, you need to register an `update` event handler and update the * `VisualizeJS` viewer and active dragger manually. Default is `true`. * @param params.markupType - The type of the markup core: `Visualize` (deprecated) or `Konva`. Default * is `Konva`. */ constructor( client?: Client, params: { visualizeJsUrl?: string; crossOrigin?: string; enableAutoUpdate?: boolean; markupType?: MarkupType } = {} ) { super(); this.configure(params); this._options = new Options(this); this.client = client; this.loaders = []; this._activeDragger = null; this._components = []; this._renderTime = 0; this.canvasEvents = CANVAS_EVENTS.slice(); this.canvaseventlistener = (event: Event) => this.emit(event); this._enableAutoUpdate = params.enableAutoUpdate ?? true; this._isNeedRender = false; this._isRunAsyncUpdate = false; this.render = this.render.bind(this); this.update = this.update.bind(this); this._markup = MarkupFactory.createMarkup(params.markupType); } /** * Viewer options. */ get options(): IOptions { return this._options; } /** * `VisualizeJS` library URL. Use {@link configure | configure()} to change library URL. * * @readonly */ get visualizeJsUrl(): string { return this._visualizeJsUrl; } /** * 2D markup core instance used to create markups. * * @readonly */ get markup(): IMarkup { return this._markup; } /** * Changes the viewer parameters. * * @param params - An object containing new parameters. * @param params.visualizeJsUrl - `VisualizeJS` library URL. Set this URL to use your own library * instance or specify `undefined` or blank to use the default URL defined by `Viewer.visualize` * library you are using. * @param params.crossOrigin - The * {@link https://developer.mozilla.org/docs/Web/HTML/Attributes/crossorigin | crossorigin} content * attribute on `Visalize.js` script element. One of the following values: `""`, `anonymous` or * `use-credentials`. */ configure(params: { visualizeJsUrl?: string; crossOrigin?: string }): this { this._visualizeJsUrl = params.visualizeJsUrl || "VISUALIZE_JS_URL"; this._crossOrigin = params.crossOrigin; return this; } /** * Loads the `VisualizeJS` module and initializes it with the specified canvas. Call * {@link dispose | dispose()} to release allocated resources. * * Fires: * * - {@link InitializeEvent | initialize} * - {@link InitializeProgressEvent | initializeprogress} * * @param canvas - * {@link https://developer.mozilla.org/docs/Web/API/HTMLCanvasElement | HTMLCanvasElement} for * `VisualizeJS`. * @param onProgress - A callback function that handles events measuring progress of loading of the * `VisualizeJS` library. */ async initialize(canvas: HTMLCanvasElement, onProgress?: (event: ProgressEvent) => void): Promise<this> { this.addEventListener("optionschange", (event) => this.syncOptions(event.data)); if (canvas.style.width === "" && canvas.style.height === "") { canvas.style.width = "100%"; canvas.style.height = "100%"; } canvas.parentElement.style.touchAction = "none"; canvas.style.touchAction = "none"; canvas.width = canvas.clientWidth * window.devicePixelRatio; canvas.height = canvas.clientHeight * window.devicePixelRatio; this._visualizeTimestamp = Date.now(); const visualizeTimestamp = this._visualizeTimestamp; const visualizeJs: any = await loadVisualizeJs( this.visualizeJsUrl, (event: ProgressEvent) => { const { loaded, total } = event; if (onProgress) onProgress(new ProgressEvent("progress", { lengthComputable: true, loaded, total })); this.emitEvent({ type: "initializeprogress", data: loaded / total, loaded, total }); }, { crossOrigin: this._crossOrigin } ); if (visualizeTimestamp !== this._visualizeTimestamp) throw new Error( "Viewer error: dispose() was called before initialize() completed. Are you using React strict mode?" ); this._visualizeJs = visualizeJs; this.visualizeJs.canvas = canvas; this.visualizeJs.Viewer.create(); this.visualizeJs.getViewer().resize(0, canvas.width, canvas.height, 0); this.canvas = canvas; this.canvasEvents.forEach((x) => canvas.addEventListener(x, this.canvaseventlistener)); this._markup.initialize(this.canvas, this.canvasEvents, this, this); for (const name of components.getComponents().keys()) { this._components.push(components.createComponent(name, this)); } this.syncOpenCloudVisualStyle(true); this.syncOptions(); this.syncOverlay(); this._renderTime = performance.now(); this.render(this._renderTime); this.emitEvent({ type: "initialize" }); return this; } dispose(): this { this.cancel(); this.clear(); this.emitEvent({ type: "dispose" }); this.removeAllListeners(); this.setActiveDragger(); this._components.forEach((component: IComponent) => component.dispose()); this._components = []; this._markup.dispose(); if (this.canvas) { this.canvasEvents.forEach((x) => this.canvas.removeEventListener(x, this.canvaseventlistener)); this.canvas = undefined; } if (this._visualizeJs) this._visualizeJs.getViewer().clear(); this._visualizeJs = undefined; this._visualizeTimestamp = undefined; return this; } /** * Returns `true` if `VisualizeJS` module has been loaded and initialized. */ isInitialized(): boolean { return !!this.visualizeJs; } // internal render/resize routines public render(time: DOMHighResTimeStamp) { if (!this.visualizeJs) return; if (this._isRunAsyncUpdate) return; const visViewer = this.visualizeJs.getViewer(); if (visViewer.isRunningAnimation() || this._isNeedRender) { visViewer.update(); this._activeDragger?.updatePreview?.(); this._isNeedRender = !visViewer.getActiveDevice().isValid(); const deltaTime = (time - this._renderTime) / 1000; this._renderTime = time; this.emitEvent({ type: "render", time, deltaTime }); } } public resize(): this { if (!this.visualizeJs) return this; const { clientWidth, clientHeight } = this.canvas; if (!clientWidth || !clientHeight) return this; // <- invisible viewer, or viewer with parent removed this.canvas.width = clientWidth * window.devicePixelRatio; this.canvas.height = clientHeight * window.devicePixelRatio; const visViewer = this.visualizeJs.getViewer(); visViewer.resize(0, this.canvas.width, this.canvas.height, 0); this.update(true); this.emitEvent({ type: "resize", width: clientWidth, height: clientHeight }); return this; } /** * Updates the viewer. * * Do nothing if the auto-update mode is disabled in the constructor. In this case, register an * `update` event handler and update the `Visualize` viewer and active dragger manually. * * Fires: * * - {@link UpdateEvent | update} * * @param force - If `true` updates the viewer immidietly. Otherwise updates on next animation frame. * Default is `false`. */ update(force = false) { if (this._enableAutoUpdate) { if (force) { this.visViewer()?.update(); this._activeDragger?.updatePreview?.(); } else { this._isNeedRender = true; } } this.emitEvent({ type: "update", data: force }); } private scheduleUpdateAsync(maxScheduleUpdateTimeInMs = 50): Promise<void> { return new Promise<void>((resolve, reject) => { setTimeout(() => { try { if (this._enableAutoUpdate) { this.visViewer()?.update(maxScheduleUpdateTimeInMs); this._activeDragger?.updatePreview?.(); } this.emitEvent({ type: "update", data: false }); resolve(); } catch (e) { console.error(e); reject(); } }, 0); }); } /** * Updates the viewer asynchronously without locking the user interface. Used to update the viewer * after changes that require a long rendering time. * * Do nothing if the auto-update mode is disabled in the constructor. In this case, register an * `update` event handler and update the `VisualizeJS` viewer and active dragger manually. * * Fires: * * - {@link UpdateEvent | update} * * @param maxScheduleUpdateTimeInMs - Maximum time for one update, default 30 ms. * @param maxScheduleUpdateCount - Maximum count of scheduled updates. */ async updateAsync(maxScheduleUpdateTimeInMs = 50, maxScheduleUpdateCount = 50): Promise<void> { this._isRunAsyncUpdate = true; const device = this.visViewer().getActiveDevice(); try { for (let iterationCount = 0; !device.isValid() && iterationCount < maxScheduleUpdateCount; iterationCount++) { await this.scheduleUpdateAsync(maxScheduleUpdateTimeInMs); } await this.scheduleUpdateAsync(maxScheduleUpdateTimeInMs); } catch (e) { console.error(e); } finally { this._isRunAsyncUpdate = false; } } /** * Returns `VisualizeJS` {@link https://cloud.opendesign.com/docs/index.html#/visualizejs_api | module} * instance. */ get visualizeJs(): any { return this._visualizeJs; } /** * Returns `VisualizeJS` {@link https://cloud.opendesign.com/docs/index.html#/visualizejs_api | module} * instance. */ visLib(): any { return this.visualizeJs; } /** * Returns `VisualizeJS` {@link https://cloud.opendesign.com/docs/index.html#/vis/Viewer | Viewer} * instance. */ visViewer(): any { return this.visualizeJs?.getViewer(); } // update the VisualizeJS options syncOpenCloudVisualStyle(isInitializing: boolean): this { if (!this.visualizeJs) return this; const visLib = this.visLib(); const visViewer = visLib.getViewer(); const device = visViewer.getActiveDevice(); if (device.isNull()) return this; const view = device.getActiveView(); view.enableDefaultLighting(true, visLib.DefaultLightingType.kTwoLights); view.setDefaultLightingIntensity(1.25); // Visualize.js 25.11 and earlier threw an exception if the style did not exist. let visualStyleId; try { visualStyleId = visViewer.findVisualStyle("OpenCloud"); } catch { visualStyleId = undefined; } if (!visualStyleId || visualStyleId.isNull()) { visualStyleId = visViewer.createVisualStyle("OpenCloud"); const colorDef = new visLib.OdTvColorDef(66, 66, 66); const shadedVsId = visViewer.findVisualStyle("Realistic"); const visualStylePtr = visualStyleId.openObject(); visualStylePtr.copyFrom(shadedVsId); visualStylePtr.setOptionInt32(visLib.VisualStyleOptions.kFaceModifiers, 0, visLib.VisualStyleOperations.kSet); visualStylePtr.setOptionInt32(visLib.VisualStyleOptions.kEdgeModel, 2, visLib.VisualStyleOperations.kSet); visualStylePtr.setOptionDouble(visLib.VisualStyleOptions.kEdgeCreaseAngle, 60, visLib.VisualStyleOperations.kSet); visualStylePtr.setOptionInt32(visLib.VisualStyleOptions.kEdgeStyles, 0, visLib.VisualStyleOperations.kSet); visualStylePtr.setOptionInt32(visLib.VisualStyleOptions.kEdgeModifiers, 8, visLib.VisualStyleOperations.kSet); visualStylePtr.setOptionColor( visLib.VisualStyleOptions.kEdgeColorValue, colorDef, visLib.VisualStyleOperations.kSet ); visualStylePtr.delete(); } view.visualStyle = visualStyleId; view.delete(); device.delete(); return this; } syncOptions(options: IOptions = this.options): this { if (!this.visualizeJs) return this; const visLib = this.visLib(); const visViewer = visLib.getViewer(); const device = visViewer.getActiveDevice(); if (device.isNull()) return this; if (options.showWCS !== visViewer.getEnableWCS()) { visViewer.setEnableWCS(options.showWCS); } if (options.cameraAnimation !== visViewer.getEnableAnimation()) { visViewer.setEnableAnimation(options.cameraAnimation); } const antialiasing = options.antialiasing === true || options.antialiasing === "fxaa"; if (antialiasing !== visViewer.fxaaAntiAliasing3d) { visViewer.fxaaAntiAliasing3d = antialiasing; visViewer.fxaaQuality = 5; } if (options.shadows !== visViewer.shadows) { visViewer.shadows = options.shadows; const canvas = visLib.canvas; device.invalidate([0, canvas.clientWidth, canvas.clientHeight, 0]); } if (options.groundShadow !== visViewer.groundShadow) { visViewer.groundShadow = options.groundShadow; } if (options.ambientOcclusion !== device.getOptionBool(visLib.DeviceOptions.kSSAOEnable)) { device.setOptionBool(visLib.DeviceOptions.kSSAOEnable, options.ambientOcclusion); device.setOptionBool(visLib.DeviceOptions.kSSAODynamicRadius, true); device.setOptionDouble(visLib.DeviceOptions.kSSAORadius, 1); device.setOptionInt32(visLib.DeviceOptions.kSSAOLoops, 32); device.setOptionDouble(visLib.DeviceOptions.kSSAOPower, 2); device.setOptionInt32(visLib.DeviceOptions.kSSAOBlurRadius, 2); const activeView = visViewer.activeView; activeView.setSSAOEnabled(options.ambientOcclusion); activeView.delete(); } if (isExist(options.edgeModel)) { const activeView = device.getActiveView(); const visualStyleId = visViewer.findVisualStyle("OpenCloud"); const visualStylePtr = visualStyleId.openObject(); visualStylePtr.setOptionInt32( visLib.VisualStyleOptions.kEdgeModel, options.edgeModel ? 2 : 0, visLib.VisualStyleOperations.kSet ); activeView.visualStyle = visualStyleId; visualStylePtr.delete(); visualStyleId.delete(); activeView.delete(); } device.delete(); this.syncHighlightingOptions(options); this.update(); return this; } syncHighlightingOptions(options: IOptions = this.options): this { if (!this.visualizeJs) return this; const params = options.enableCustomHighlight ? options : Options.defaults(); const visLib = this.visLib(); const visViewer = visLib.getViewer(); const { Entry, OdTvRGBColorDef } = visLib; const highlightStyleId = visViewer.findHighlightStyle("Web_Default"); const highlightStylePtr = highlightStyleId.openObject(); if (isExist(params.facesColor)) { const color = new OdTvRGBColorDef(params.facesColor.r, params.facesColor.g, params.facesColor.b); highlightStylePtr.setFacesColor(Entry.k3D.value | Entry.k3DTop.value, color); color.delete(); } if (isExist(params.facesOverlap)) { highlightStylePtr.setFacesVisibility(Entry.k3DTop.value, params.facesOverlap); } if (isExist(params.facesTransparancy)) { highlightStylePtr.setFacesTransparency(Entry.k3D.value | Entry.k3DTop.value, params.facesTransparancy); } if (isExist(params.edgesColor)) { const color = new OdTvRGBColorDef(params.edgesColor.r, params.edgesColor.g, params.edgesColor.b); highlightStylePtr.setEdgesColor( Entry.k3DTop.value | Entry.k3D.value | Entry.k2D.value | Entry.k2DTop.value, color ); color.delete(); } if (isExist(params.edgesVisibility)) { highlightStylePtr.setEdgesVisibility( Entry.k2D.value | Entry.k2DTop.value | Entry.k3DTop.value | Entry.k3D.value, params.edgesVisibility ); } if (isExist(params.edgesOverlap)) { const visibility = !isExist(params.edgesVisibility) ? true : params.edgesVisibility; highlightStylePtr.setEdgesVisibility(Entry.k2DTop.value | Entry.k3DTop.value, params.edgesOverlap && visibility); } const device = visViewer.getActiveDevice(); if (!device.isNull()) { const canvas = visLib.canvas; device.invalidate([0, canvas.clientWidth, canvas.clientHeight, 0]); device.delete(); } return this; } get draggers(): string[] { return [...draggers.getDraggers().keys()]; } get components(): string[] { return [...components.getComponents().keys()]; } /** * Deprecated since `25.12`. Use {@link draggers.registerDragger} instead. */ public registerDragger(name: string, dragger: typeof Dragger): void { console.warn( "Viewer.registerDragger() has been deprecated since 25.12 and will be removed in a future release, use draggers('visualizejs').registerDragger() instead." ); draggers.registerDragger(name, (viewer: IViewer) => new dragger(viewer)); } activeDragger(): IDragger | null { return this._activeDragger; } setActiveDragger(name = ""): IDragger | null { if (!this._activeDragger || this._activeDragger.name !== name) { const oldDragger = this._activeDragger; let newDragger = null; if (this._activeDragger) { this._activeDragger.dispose(); this._activeDragger = null; } if (this.visualizeJs) { newDragger = draggers.createDragger(name, this); if (newDragger) { this._activeDragger = newDragger; this._activeDragger.initialize?.(); } } const canvas = this.canvas; if (canvas) { if (oldDragger) canvas.classList.remove(`oda-cursor-${oldDragger.name.toLowerCase()}`); if (newDragger) canvas.classList.add(`oda-cursor-${newDragger.name.toLowerCase()}`); } this.emitEvent({ type: "changeactivedragger", data: name }); this.update(); } return this._activeDragger; } resetActiveDragger(): void { const dragger = this._activeDragger; if (dragger) { this.setActiveDragger(); this.setActiveDragger(dragger.name); } } getComponent(name: string): IComponent { return this._components.find((component) => component.name === name); } clearSlices(): void { if (!this.visualizeJs) return; const visViewer = this.visViewer(); const activeView = visViewer.activeView; activeView.removeCuttingPlanes(); activeView.delete(); this.update(); } clearOverlay(): void { if (!this.visualizeJs) return; this._markup.clearOverlay(); this.update(); } syncOverlay(): void { if (!this.visualizeJs) return; const visViewer = this.visViewer(); const activeView = visViewer.activeView; let overlayView = visViewer.getViewByName(OVERLAY_VIEW_NAME); if (!overlayView) { const markupModel = visViewer.getMarkupModel(); const pDevice = visViewer.getActiveDevice(); overlayView = pDevice.createView(OVERLAY_VIEW_NAME, false); overlayView.addModel(markupModel); activeView.addSibling(overlayView); pDevice.addView(overlayView); } overlayView.viewPosition = activeView.viewPosition; overlayView.viewTarget = activeView.viewTarget; overlayView.upVector = activeView.upVector; overlayView.viewFieldWidth = activeView.viewFieldWidth; overlayView.viewFieldHeight = activeView.viewFieldHeight; const viewPort = overlayView.getViewport(); overlayView.setViewport(viewPort.lowerLeft, viewPort.upperRight); overlayView.vportRect = activeView.vportRect; this._markup.syncOverlay(); this.update(); } is3D(): boolean { if (!this.visualizeJs) return false; const visViewer = this.visViewer(); const ext = visViewer.getActiveExtents(); const min = ext.min(); const max = ext.max(); const extHeight = max[2] - min[2]; return extHeight !== 0; //return visViewer.activeView.upVector[1] >= 0.95; } screenToWorld(position: { x: number; y: number }): { x: number; y: number; z: number } { if (!this.visualizeJs) return { x: position.x, y: position.y, z: 0 }; const activeView = this.visViewer().activeView; const worldPoint = activeView.transformScreenToWorld( position.x * window.devicePixelRatio, position.y * window.devicePixelRatio ); const result = { x: worldPoint[0], y: worldPoint[1], z: worldPoint[2] }; activeView.delete(); return result; } worldToScreen(position: { x: number; y: number; z: number }): { x: number; y: number } { if (!this.visualizeJs) return { x: position.x, y: position.y }; const activeView = this.visViewer().activeView; const devicePoint = activeView.transformWorldToScreen(position.x, position.y, position.z); const result = { x: devicePoint[0] / window.devicePixelRatio, y: devicePoint[1] / window.devicePixelRatio }; activeView.delete(); return result; } getScale(): { x: number; y: number; z: number } { const result = { x: 1.0, y: 1.0, z: 1.0 }; const projMatrix = this.visViewer().activeView.projectionMatrix; const tolerance = 1.0e-6; const x = projMatrix.get(0, 0); if (x > tolerance || x < -tolerance) result.x = 1 / x; const y = projMatrix.get(1, 1); if (y > tolerance || y < -tolerance) result.y = 1 / y; const z = projMatrix.get(2, 2); if (z > tolerance || z < -tolerance) result.z = 1 / z; return result; } getSelected(): string[] { return this.executeCommand("getSelected"); } setSelected(handles?: string[]): void { this.executeCommand("setSelected", handles); } clearSelected(): void { this.executeCommand("clearSelected"); } hideSelected(): void { this.executeCommand("hideSelected"); } isolateSelected(): void { this.executeCommand("isolateSelected"); } showAll(): void { this.executeCommand("showAll"); } explode(index = 0): void { this.executeCommand("explode", index); } collect(): void { this.executeCommand("collect"); } // Internal loading routines async loadReferences(model: Model | File | Assembly): Promise<this> { if (!this.visualizeJs) return this; if (!this.client) return this; if (!model.getReferences) return this; const abortController = new AbortController(); this._abortControllerForReferences?.abort(); this._abortControllerForReferences = abortController; let references: any[] = []; await model .getReferences(abortController.signal) .then((data) => (references = data.references)) .catch((e) => console.error("Cannot load model references.", e)); for (const file of references) { await this.client .downloadFile(file.id, undefined, abortController.signal) .then((arrayBuffer) => this.visualizeJs?.getViewer().addEmbeddedFile(file.name, new Uint8Array(arrayBuffer))) .catch((e) => console.error(`Cannot load reference file ${file.name}.`, e)); } return this; } applyModelTransformMatrix(model: Model | Assembly) { this.executeCommand("applyModelTransform", model); } applySceneGraphSettings(options = this.options) { if (!this.visualizeJs) return; const visLib = this.visLib(); const visViewer = visLib.getViewer(); const device = visViewer.getActiveDevice(); if (isExist(options.sceneGraph)) { device.setOptionBool(visLib.DeviceOptions.kDelaySceneGraphProc, !options.sceneGraph); } // if (options.enablePartialMode && visLib.HpTrc.Usd >= visViewer.memoryLimit) { // device.setOptionBool(visLib.DeviceOptions.kDelaySceneGraphProc, true); // } device.delete(); this.update(); } /** * Loads a file into the viewer. * * The viewer must be {@link initialize | initialized} before opening the file. Otherwise, `open()` does * nothing. * * This method requires a `Client` instance to be specified to load file from the Open Cloud Server. * The file geometry data on the Open Cloud Server must be converted into a `vsfx` format, otherwise an * exception will be thrown. * * For files from Open Cloud Server, the default model will be loaded. If there is no default model, * first availiable model will be loaded. If no models are found in the file, an exception will be * thrown. * * For URLs, the file extension is used to determine the file format. For a `ArrayBuffer` and `Data * URL`, a file format must be specified using `params.format` parameter (see below). If no appropriate * loader is found for the specified format, an exception will be thrown. * * If there was an active dragger before opening the file, it will be deactivated. After opening the * file, you must manually activate the required dragger. * * To open a large files, enable {@link IOptions.enablePartialMode | partial streaming} mode before * opening. Partial streaming is only supported when opening files from an Open Cloud Server, but not * local files and URLs. Example: * * ```javascript * viewer.options.enableStreamingMode = true; * viewer.options.enablePartialMode = true; * await viewer.open(file); * ``` * * Fires: * * - {@link OpenEvent | open} * - {@link GeometryStartEvent | geometrystart} * - {@link GeometryProgressEvent | geometryprogress} * - {@link DatabaseChunkEvent | databasechunk} * - {@link GeometryChunkEvent | geometrychunk} * - {@link GeometryEndEvent | geometryend} * - {@link GeometryErrorEvent | geometryerror} * * @param file - File to load. Can be one of: * * - `File`, `Assembly` or `Model` instance from the Open Cloud Server * - File `URL` string * - {@link https://developer.mozilla.org/docs/Web/HTTP/Basics_of_HTTP/Data_URIs | Data URL} string * - {@link https://developer.mozilla.org/docs/Web/API/File | Web API File} object * - {@link https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/ArrayBuffer | ArrayBuffer} * object * * @param params - Loading parameters. * @param params.format - File format string. Required when loading a file as `ArrayBuffer` or `Data * URL`. * @param params.mode - Reserved for future use. * @param params.requestHeader - The * {@link https://developer.mozilla.org/docs/Glossary/Request_header | request header} used in HTTP * request. * @param params.withCredentials - Whether the HTTP request uses credentials such as cookies, * authorization headers or TLS client certificates. See * {@link https://developer.mozilla.org/docs/Web/API/XMLHttpRequest/withCredentials | XMLHttpRequest.withCredentials} * for more details. */ async open( file: FileSource, params: { format?: string; mode?: string; requestHeader?: HeadersInit; withCredentials?: boolean; } = {} ): Promise<this> { if (!this.visualizeJs) return this; this.cancel(); this.clear(); this.emitEvent({ type: "open", file }); let model: any = file; if (model && typeof model.getModels === "function") { const models = await model.getModels(); model = models.find((model: Model) => model.default) || models[0] || file; } if (!model) throw new Error(`Format not supported`); let format = params.format; if (!format && typeof model.type === "string") format = model.type.split(".").pop(); if (!format && typeof file === "string") format = file.split(".").pop(); if (!format && file instanceof globalThis.File) format = file.name.split(".").pop(); const loader = loaders.createLoader(this, model, format); if (!loader) throw new Error(`Format not supported`); this.loaders.push(loader); this.emitEvent({ type: "geometrystart", file, model }); try { await this.loadReferences(model); await loader.load(model, format, params); } catch (error: any) { this.emitEvent({ type: "geometryerror", data: error, file, model }); throw error; } this.emitEvent({ type: "geometryend", file, model }); if (this.visualizeJs) { this.applyModelTransformMatrix(model); this.applySceneGraphSettings(); } return this; } /** * Deprecated since `26.4`. Use {@link open | open()} instead. * * @deprecated */ openVsfFile(buffer: Uint8Array | ArrayBuffer): this { console.warn( "Viewer.openVsfFile() has been deprecated since 26.4 and will be removed in a future release, use Viewer.open() instead." ); if (!this.visualizeJs) return this; this.cancel(); this.clear(); this.emitEvent({ type: "open", file: buffer }); const visLib = this.visLib(); const visViewer = visLib.getViewer(); this.emitEvent({ type: "geometrystart", file: buffer }); try { const data = buffer instanceof Uint8Array ? buffer : new Uint8Array(buffer); visViewer.parseFile(data); this.syncOpenCloudVisualStyle(false); this.syncOptions(); this.syncOverlay(); this.resize(); this.emitEvent({ type: "geometryprogress", data: 1, file: buffer }); this.emitEvent({ type: "databasechunk", data, file: buffer }); } catch (error: any) { this.emitEvent({ type: "geometryerror", data: error, file: buffer }); throw error; } this.emitEvent({ type: "geometryend", file: buffer }); return this; } /** * Deprecated since `26.4`. Use {@link open | open()} instead. * * @deprecated */ openVsfxFile(buffer: Uint8Array | ArrayBuffer): this { console.warn( "Viewer.openVsfxFile() has been deprecated since 26.4 and will be removed in a future release, use Viewer.open() instead." ); if (!this.visualizeJs) return this; this.cancel(); this.clear(); this.emitEvent({ type: "open", file: buffer }); const visLib = this.visLib(); const visViewer = visLib.getViewer(); this.emitEvent({ type: "geometrystart", file: buffer }); try { const data = buffer instanceof Uint8Array ? buffer : new Uint8Array(buffer); visViewer.parseVsfx(data); this.syncOpenCloudVisualStyle(false); this.syncOptions(); this.syncOverlay(); this.resize(); this.emitEvent({ type: "geometryprogress", data: 1, file: buffer }); this.emitEvent({ type: "databasechunk", data, file: buffer }); } catch (error: any) { this.emitEvent({ type: "geometryerror", data: error, file: buffer }); throw error; } this.emitEvent({ type: "geometryend", file: buffer }); return this; } cancel(): this { this._abortControllerForReferences?.abort(); this._abortControllerForReferences = undefined; this.loaders.forEach((loader) => loader.cancel()); this.emitEvent({ type: "cancel" }); return this; } clear(): this { if (!this.visualizeJs) return this; const visLib = this.visLib(); const visViewer = visLib.getViewer(); this.setActiveDragger(); this.clearSlices(); this.clearOverlay(); this.clearSelected(); visViewer.clear(); visViewer.createLocalDatabase(); this.loaders.forEach((loader) => loader.dispose()); this.loaders = []; this.syncOpenCloudVisualStyle(true); this.syncOptions(); this.syncOverlay(); this.resize(); this.emitEvent({ type: "clear" }); return this; } /** * Deprecated since `25.11`. Use {@link IMarkup.getMarkupColor | markup.getMarkupColor()} instead. */ getMarkupColor(): { r: number; g: number; b: number } { console.warn( "Viewer.getMarkupColor() has been deprecated since 25.11 and will be removed in a future release, use Viewer.markup.getMarkupColor() instead." ); return this._markup.getMarkupColor(); } /** * Deprecated since `25.11`. Use {@link IMarkup.setMarkupColor | markup.setMarkupColor()} instead. */ setMarkupColor(r = 255, g = 0, b = 0): void { console.warn( "Viewer.setMarkupColor() has been deprecated since 25.11 and will be removed in a future release, use Viewer.markup.setMarkupColor() instead." ); this._markup.setMarkupColor(r, g, b); } /** * Deprecated since `25.11`. Use {@link IMarkup.colorizeAllMarkup | markup.colorizeAllMarkup()} instead. */ colorizeAllMarkup(r = 255, g = 0, b = 0): void { console.warn( "Viewer.colorizeAllMarkup() has been deprecated since 25.11 and will be removed in a future release, use Viewer.markup.colorizeAllMarkup() instead." ); this._markup.colorizeAllMarkup(r, g, b); } /** * Deprecated since `25.11`. Use * {@link IMarkup.colorizeSelectedMarkups | markup.colorizeSelectedMarkups()} instead. */ colorizeSelectedMarkups(r = 255, g = 0, b = 0): void { this._markup.colorizeSelectedMarkups(r, g, b); } /** * Adds an empty `Visualize` markup entity to the overlay. */ addMarkupEntity(entityName: string) { if (!this.visualizeJs) return null; this.syncOverlay(); const visViewer = this.visViewer(); const model = visViewer.getMarkupModel(); const entityId = model.appendEntity(entityName); const entityPtr = entityId.openObject(); const color = this.getMarkupColor(); entityPtr.setColor(color.r, color.g, color.b); entityPtr.setLineWeight(2); entityPtr.delete(); this.update(); return entityId; } drawViewpoint(viewpoint: IViewpoint): void { if (!this.visualizeJs) return; const draggerName = this._activeDragger?.name; this.setActiveDragger(); this.clearSlices(); this.clearOverlay(); this.clearSelected(); this.showAll(); this.explode(); this.setOrthogonalCameraSettings(viewpoint.orthogonal_camera); this.setClippingPlanes(viewpoint.clipping_planes); this.setSelection(viewpoint.selection); this._markup.setViewpoint(viewpoint); this.setActiveDragger(draggerName); this.emitEvent({ type: "drawviewpoint", data: viewpoint }); this.update(); } createViewpoint(): IViewpoint { if (!this.visualizeJs) return {}; const viewpoint: IViewpoint = {}; viewpoint.orthogonal_camera = this.getOrthogonalCameraSettings(); viewpoint.clipping_planes = this.getClippingPlanes(); viewpoint.selection = this.getSelection(); viewpoint.description = new Date().toDateString(); this._markup.getViewpoint(viewpoint); this.emitEvent({ type: "createviewpoint", data: viewpoint }); return viewpoint; } private getPoint3dFromArray(array: number[]) { return { x: array[0], y: array[1], z: array[2] }; } private getLogicalPoint3dAsArray(point3d: IPoint) { return [point3d.x, point3d.y, point3d.z]; } private getOrthogonalCameraSettings(): IOrthogonalCamera { const visViewer = this.visViewer(); const activeView = visViewer.activeView; return { view_point: this.getPoint3dFromArray(activeView.viewPosition), direction: this.getPoint3dFromArray(activeView.viewTarget), up_vector: this.getPoint3dFromArray(activeView.upVector), field_width: activeView.viewFieldWidth, field_height: activeView.viewFieldHeight, view_to_world_scale: 1, }; } private setOrthogonalCameraSettings(settings: IOrthogonalCamera) { const visViewer = this.visViewer(); const activeView = visViewer.activeView; if (settings) { activeView.setView( this.getLogicalPoint3dAsArray(settings.view_point), this.getLogicalPoint3dAsArray(settings.direction), this.getLogicalPoint3dAsArray(settings.up_vector), settings.field_width, settings.field_height, true ); this.syncOverlay(); } } private getClippingPlanes(): IClippingPlane[] { const visViewer = this.visViewer(); const activeView = visViewer.activeView; const clipping_planes = []; for (let i = 0; i < activeView.numCuttingPlanes(); i++) { const cuttingPlane = activeView.getCuttingPlane(i); const clipping_plane = { location: this.getPoint3dFromArray(cuttingPlane.getOrigin()), direction: this.getPoint3dFromArray(cuttingPlane.normal()), }; clipping_planes.push(clipping_plane); } return clipping_planes; } private setClippingPlanes(clipping_planes: IClippingPlane[]) { if (clipping_planes) { const visViewer = this.visViewer(); const activeView = visViewer.activeView; for (const clipping_plane of clipping_planes) { const cuttingPlane = new (this.visLib().OdTvPlane)(); cuttingPlane.set( this.getLogicalPoint3dAsArray(clipping_plane.location), this.getLogicalPoint3dAsArray(clipping_plane.direction) ); activeView.addCuttingPlane(cuttingPlane); activeView.setEnableCuttingPlaneFill(true, 0x66, 0x66, 0x66); } } } private getSelection(): IEntity[] { return this.getSelected().map((handle) => ({ handle })); } private setSelection(selection: IEntity[]) { this.setSelected(selection?.map((component) => component.handle)); } /** * Executes the command denoted by the given command. If the command is not found, tries to set active * dragger with the specified name. * * The following commands are available by default: * * - `applyModelTransform` * - `autoTransformAllModelsToCentralPoint` * - `clearMarkup` * - `clearSelected` * - `clearSlices` * - `createPreview` * - `explode` * - `getDefaultViewPositions` * - `getModels` * - `getSelected` * - `hideSelected` * - `isolateSelected` * - `regenerateAll` * - `resetView` * - `selectModel` * - `setActiveDragger` * - `setDefaultViewPosition` * - `setMarkupColor` * - `setSelected` * - `showAll` * - `zoomToExtents` * - `zoomToObjects` * - `zoomToSelected` * * To register custom command use the {@link commands.registerCommand}. * * @param id - Command ID or dragger name. * @param args - Parameters passed to the command handler function. * @returns Returns the result of the command handler function or new active dragger instance. Returns * `undefined` if neither the command nor the dragger exists. */ executeCommand(id: string, ...args: any[]): any { return commands.executeCommand(id, this, ...args); } public deviceAutoRegeneration() { const visViewer = this.visViewer(); const device = visViewer.getActiveDevice(); const coef = device.getOptionDouble(this.visLib().DeviceOptions.kRegenCoef); if (coef > 1.0) { visViewer.regenAll(); this.update(); } } }