UNPKG

@inweb/viewer-visualize

Version:

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

1,423 lines (1,147 loc) 46 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, IDragger, IEntity, IInfo, ILoader, Info, IOrthogonalCamera, IOptions, IPerspectiveCamera, 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 { IModelImpl } from "./Models/IModelImpl"; 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}. */ export class Viewer extends EventEmitter2<ViewerEventMap & CanvasEventMap & OptionsEventMap> implements IViewer, IWorldTransform { public client: Client | undefined; public options: IOptions; public canvas: HTMLCanvasElement | undefined; public canvasEvents: string[]; public loaders: ILoader[]; public models: IModelImpl[]; public info: IInfo; private canvaseventlistener: (event: Event) => void; private _visualizeJsUrl = ""; private _visualizeJs: any; private _visualizeTimestamp: number; private _viewer: any; private _crossOrigin; private _activeDragger: IDragger | null; private _components: IComponent[]; private _updateDelay: number; private _renderNeeded: boolean; private _renderTime: DOMHighResTimeStamp; private _enableAutoUpdate: boolean; private _isRunAsyncUpdate: boolean; public _abortControllerForReferences: AbortController | undefined; private _markup: IMarkup; /** * @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.client = client; this.options = new Options(this); this.loaders = []; this.models = []; this.info = new Info(); this.canvasEvents = CANVAS_EVENTS.slice(); this.canvaseventlistener = (event: Event) => this.emit(event); this._activeDragger = null; this._components = []; this._updateDelay = 1000; this._renderNeeded = false; this._renderTime = 0; this._enableAutoUpdate = params.enableAutoUpdate ?? true; this._isRunAsyncUpdate = false; this.render = this.render.bind(this); this.update = this.update.bind(this); this._markup = MarkupFactory.createMarkup(params.markupType); } /** * `VisualizeJS` library URL. Use {@link configure | configure()} to change library URL. * * @readonly */ get visualizeJsUrl(): string { return this._visualizeJsUrl; } /** * 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._viewer; } /** * 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; } // IViewer get draggers(): string[] { return [...draggers.getDraggers().keys()]; } get components(): string[] { return [...components.getComponents().keys()]; } /** * 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)); const pixelRatio = window.devicePixelRatio; const rect = canvas.parentElement.getBoundingClientRect(); const width = rect.width || 1; const height = rect.height || 1; canvas.width = Math.round(width * pixelRatio); canvas.height = Math.round(height * pixelRatio); canvas.style.width = width + "px"; canvas.style.height = height + "px"; canvas.parentElement.style.touchAction = "none"; canvas.style.touchAction = "none"; 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._viewer = visualizeJs.Viewer.create(); this._viewer.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.syncOptions(); this.syncOverlay(); this._renderTime = performance.now(); this.emitEvent({ type: "initialize" }); this.update(true); 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._viewer) this._viewer.clear(); this._visualizeJs = undefined; this._visualizeTimestamp = undefined; this._viewer = undefined; return this; } /** * Returns `true` if `VisualizeJS` module has been loaded and initialized. */ isInitialized(): boolean { return !!this.visualizeJs; } setSize(width: number, height: number, updateStyle = true): void { if (!this.visualizeJs) return; this.canvas.width = Math.round(width * window.devicePixelRatio); this.canvas.height = Math.round(height * window.devicePixelRatio); if (updateStyle) { this.canvas.style.width = width + "px"; this.canvas.style.height = height + "px"; } this._viewer.resize(0, this.canvas.width, this.canvas.height, 0); this.emitEvent({ type: "resize", width, height }); this.update(true); } resize(): this { console.warn( "Viewer.resize() has been deprecated since 26.9 and will be removed in a future release, use Viewer.setSize() instead." ); if (!this.visualizeJs) return this; if (!this.canvas.parentElement) return this; const { width, height } = this.canvas.parentElement.getBoundingClientRect(); if (!width || !height) return this; // <- invisible viewer, or viewer with parent removed this.setSize(width, height); 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) { const time = performance.now(); force = force || time - this._renderTime >= this._updateDelay; if (this._enableAutoUpdate) { this._renderNeeded = true; if (force) this.render(time); } this.emitEvent({ type: "update", force }); } // Internal render routines render(time?: DOMHighResTimeStamp) { if (!this.visualizeJs) return; if (this._isRunAsyncUpdate) return; const renderNeeded = this.visViewer().isRunningAnimation() || this._renderNeeded; if (!renderNeeded) return; if (!time) time = performance.now(); const deltaTime = (time - this._renderTime) / 1000; this._renderTime = time; this._renderNeeded = !this.visViewer().getActiveDevice().isValid(); this.visViewer().update(); this._activeDragger?.updatePreview?.(); this.emitEvent({ type: "render", time, deltaTime }); } // 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) { if (!this.visualizeJs) return; this.executeCommand("applyModelTransform", model); } applySceneGraphSettings(options = this.options) { if (!this.visualizeJs) return; const visLib = this.visLib(); const visViewer = this.visViewer(); 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. 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 CancelEvent | cancel} * - {@link ClearEvent | clear} * - {@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: * * - `File`, `Assembly` or `Model` instance from the Open Cloud Server * - `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 dFile} object * - {@link https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/ArrayBuffer | ArrayBuffer} * object * * @param params - Loading parameters. * @param params.format - File format. Can be one of `vsf` or `vsfx`. Required when loading a file as * `ArrayBuffer` or `Data URL`. * @param params.mode - Reserved for future use. * @param params.modelId - 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; modelId?: string; requestHeader?: HeadersInit; withCredentials?: boolean; } = {} ): Promise<this> { if (!this.visualizeJs) return this; this.cancel(); this.clear(); this.emitEvent({ type: "open", mode: "file", 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 && typeof model.database === "string") { file = model.file; } if (!model) throw new Error(`Format not supported`); let format = params.format; if (!format && typeof file["type"] === "string") format = file["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); this.applyModelTransformMatrix(model); this.applySceneGraphSettings(); } catch (error: any) { this.emitEvent({ type: "geometryerror", data: error, file, model }); throw error; } this.emitEvent({ type: "geometryend", file, model }); this.update(true); 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", mode: "file", file: "", buffer }); const visViewer = this.visViewer(); this.emitEvent({ type: "geometrystart", file: "", buffer }); try { const data = buffer instanceof Uint8Array ? buffer : new Uint8Array(buffer); visViewer.parseFile(data); this.syncOptions(); this.syncOverlay(); 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 }); this.update(true); 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", mode: "file", file: "", buffer }); const visViewer = this.visViewer(); this.emitEvent({ type: "geometrystart", file: "", buffer }); try { const data = buffer instanceof Uint8Array ? buffer : new Uint8Array(buffer); visViewer.parseVsfx(data); this.syncOptions(); this.syncOverlay(); 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 }); this.update(true); 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 visViewer = this.visViewer(); this.setActiveDragger(); this.clearSlices(); this.clearOverlay(); this.clearSelected(); this.loaders.forEach((loader) => loader.dispose()); this.loaders = []; this.models.forEach((model) => model.dispose()); this.models = []; visViewer.clear(); visViewer.createLocalDatabase(); this.syncOptions(); this.syncOverlay(); this.emitEvent({ type: "clear" }); this.update(true); return this; } 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; } syncOptions(options: IOptions = this.options): this { if (!this.visualizeJs) return this; const visLib = this.visLib(); const visViewer = this.visViewer(); const device = visViewer.getActiveDevice(); if (device.isNull()) return this; // sync Open Cloud visual style const view = device.getActiveView(); view.enableDefaultLighting(true, visLib.DefaultLightingType.kTwoLights); view.setDefaultLightingIntensity(1.25); let visualStyleId: any; try { visualStyleId = visViewer.findVisualStyle("OpenCloud"); } catch { // Visualize.js 25.11 and earlier threw an exception if the style did not exist. 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; // sync Visualize options 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.width, canvas.height, 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(); } // sync highlighting options const params = options.enableCustomHighlight ? options : Options.defaults(); 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 canvas = visLib.canvas; // device.invalidate([0, canvas.width, canvas.height, 0]); view.delete(); device.delete(); this.update(); return this; } 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(); } clearOverlay(): void { if (!this.visualizeJs) return; this._markup.clearOverlay(); this.update(); } clearSlices(): void { if (!this.visualizeJs) return; const visViewer = this.visViewer(); const activeView = visViewer.activeView; activeView.removeCuttingPlanes(); activeView.delete(); this.update(); } getSelected(): string[] { return this.executeCommand("getSelected"); } setSelected(handles?: string[]): void { this.executeCommand("setSelected", handles); } getSelected2(): string[] { return this.executeCommand("getSelected2"); } setSelected2(handles?: string[]): void { this.executeCommand("setSelected2", 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"); } /** * 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); } drawViewpoint(viewpoint: IViewpoint): void { if (!this.visualizeJs) return; const visViewer = this.visViewer(); const activeView = visViewer.activeView; const getPoint3dAsArray = (point3d: IPoint): number[] => { return [point3d.x, point3d.y, point3d.z]; }; const setOrthogonalCamera = (orthogonal_camera: IOrthogonalCamera) => { if (orthogonal_camera) { activeView.setView( getPoint3dAsArray(orthogonal_camera.view_point), getPoint3dAsArray(orthogonal_camera.direction), getPoint3dAsArray(orthogonal_camera.up_vector), orthogonal_camera.field_width, orthogonal_camera.field_height, true ); this.options.cameraMode = "orthographic"; this.emitEvent({ type: "changecameramode", mode: "orthographic" }); } }; const setPerspectiveCamera = (perspective_camera: IPerspectiveCamera) => { if (perspective_camera) { // ===================== AI-CODE-START ====================== // Source: Claude Sonnet 4.5 // Date: 2025-12-03 // Reviewer: roman.mochalov@opendesign.com // Issue: CLOUD-5997 // Notes: Originally AI-generated, modified manually const aspectRatio = this.canvas.width / this.canvas.height; const position = perspective_camera.view_point; const target = perspective_camera.direction; const fov = (perspective_camera.field_of_view * Math.PI) / 180; const dx = target.x - position.x; const dy = target.y - position.y; const dz = target.z - position.z; const distance = Math.sqrt(dx * dx + dy * dy + dz * dz); const fieldHeight = 2 * distance * Math.tan(fov / 2); const fieldWidth = fieldHeight * aspectRatio; // ===================== AI-CODE-END ====================== activeView.setView( getPoint3dAsArray(perspective_camera.view_point), getPoint3dAsArray(perspective_camera.direction), getPoint3dAsArray(perspective_camera.up_vector), fieldWidth, fieldHeight, false ); this.options.cameraMode = "perspective"; this.emitEvent({ type: "changecameramode", mode: "perspective" }); } }; const setClippingPlanes = (clipping_planes: IClippingPlane[]) => { if (clipping_planes) { for (const clipping_plane of clipping_planes) { const cuttingPlane = new (this.visLib().OdTvPlane)(); cuttingPlane.set(getPoint3dAsArray(clipping_plane.location), getPoint3dAsArray(clipping_plane.direction)); activeView.addCuttingPlane(cuttingPlane); activeView.setEnableCuttingPlaneFill(true, 0x66, 0x66, 0x66); } } }; const setSelection = (selection: IEntity[]) => { if (selection) this.setSelected(selection.map((component) => component.handle)); }; const draggerName = this._activeDragger?.name; this.setActiveDragger(); this.clearSlices(); this.clearOverlay(); this.clearSelected(); this.showAll(); this.explode(); setOrthogonalCamera(viewpoint.orthogonal_camera); setPerspectiveCamera(viewpoint.perspective_camera); setClippingPlanes(viewpoint.clipping_planes); setSelection(viewpoint.custom_fields?.selection2 || viewpoint.selection); this._markup.setViewpoint(viewpoint); this.syncOverlay(); this.setActiveDragger(draggerName); this.emitEvent({ type: "drawviewpoint", data: viewpoint }); this.update(true); } createViewpoint(): IViewpoint { if (!this.visualizeJs) return {}; const visViewer = this.visViewer(); const activeView = visViewer.activeView; const getPoint3dFromArray = (array: number[]): IPoint => { return { x: array[0], y: array[1], z: array[2] }; }; const getOrthogonalCamera = (): IOrthogonalCamera => { if (!activeView.perspective) return { view_point: getPoint3dFromArray(activeView.viewPosition), direction: getPoint3dFromArray(activeView.viewTarget), up_vector: getPoint3dFromArray(activeView.upVector), field_width: activeView.viewFieldWidth, field_height: activeView.viewFieldHeight, view_to_world_scale: 1, }; else return undefined; }; const getPerspectiveCamera = (): IPerspectiveCamera => { if (activeView.perspective) { // ===================== AI-CODE-START ====================== // Source: Claude Sonnet 4.5 // Date: 2025-12-03 // Reviewer: roman.mochalov@opendesign.com // Issue: CLOUD-5997 // Notes: Originally AI-generated, modified manually const position = activeView.viewPosition; const target = activeView.viewTarget; const fieldHeight = activeView.viewFieldHeight; const dx = target[0] - position[0]; const dy = target[1] - position[1]; const dz = target[2] - position[2]; const distance = Math.sqrt(dx * dx + dy * dy + dz * dz); const fov = 2 * Math.atan(fieldHeight / (2 * distance)); // ===================== AI-CODE-END ====================== return { view_point: getPoint3dFromArray(activeView.viewPosition), direction: getPoint3dFromArray(activeView.viewTarget), up_vector: getPoint3dFromArray(activeView.upVector), field_of_view: (fov * 180) / Math.PI, }; } else return undefined; }; const getClippingPlanes = (): IClippingPlane[] => { const clipping_planes = []; for (let i = 0; i < activeView.numCuttingPlanes(); i++) { const cuttingPlane = activeView.getCuttingPlane(i); const clipping_plane = { location: getPoint3dFromArray(cuttingPlane.getOrigin()), direction: getPoint3dFromArray(cuttingPlane.normal()), }; clipping_planes.push(clipping_plane); } return clipping_planes; }; const getSelection = (): IEntity[] => { return this.getSelected().map((handle) => ({ handle })); }; const getSelection2 = (): IEntity[] => { return this.getSelected2().map((handle) => ({ handle })); }; const viewpoint: IViewpoint = { custom_fields: {} }; viewpoint.orthogonal_camera = getOrthogonalCamera(); viewpoint.perspective_camera = getPerspectiveCamera(); viewpoint.clipping_planes = getClippingPlanes(); viewpoint.selection = getSelection(); viewpoint.description = new Date().toDateString(); this._markup.getViewpoint(viewpoint); viewpoint.custom_fields.selection2 = getSelection2(); this.emitEvent({ type: "createviewpoint", data: viewpoint }); return viewpoint; } // IWorldTransform 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; } // ICommandService /** * 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); } // VisualizeJS viewer specific /** * Adds an empty `Visualize` markup entity to the VisualizeJS 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; } /** * 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); } 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", force: 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> { if (!this.visualizeJs) return; this._isRunAsyncUpdate = true; try { const device = this.visViewer().getActiveDevice(); 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; } } 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(); } } }