/////////////////////////////////////////////////////////////////////////////// // 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 { IViewer } from "./IViewer"; import { commands } from "./Commands"; import { EventEmitter2 } from "./EventEmitter2"; import { ViewerEventMap } from "./ViewerEvents"; import { OptionsEventMap, OptionsChangeEvent } from "./OptionsEvents"; import { CANVAS_EVENTS } from "./CanvasEvents"; import { OdBaseDragger } from "./Draggers/Common/OdBaseDragger"; import { MeasureLineDragger } from "./Draggers/MeasureLineDragger/index"; import { OdaWalkDragger } from "./Draggers/OdaWalkDragger"; import { OdCuttingPlaneXAxisDragger } from "./Draggers/OdCuttingPlaneXAxisDragger"; import { OdCuttingPlaneYAxisDragger } from "./Draggers/OdCuttingPlaneYAxisDragger"; import { OdCuttingPlaneZAxisDragger } from "./Draggers/OdCuttingPlaneZAxisDragger"; import { OdOrbitDragger } from "./Draggers/OdOrbitDragger"; import { OdPanDragger } from "./Draggers/OdPanDragger"; import { OdZoomDragger } from "./Draggers/OdZoomDragger"; import { OdZoomWheelDragger } from "./Draggers/OdZoomWheelDragger"; import { OdZoomWindowDragger } from "./Draggers/OdZoomWindowDragger/index"; import { OrbitAroundBuildingDragger } from "./Draggers/OrbitAroundBuildingDragger"; import { GestureManager } from "./Draggers/Common/GestureManager"; import { loadVisualizeJs } from "./utils"; import { Client } from "../Api/Client"; import { File } from "../Api/File"; import { Assembly } from "../Api/Assembly"; import { Model } from "../Api/Model"; import { Options, OptionsData } from "./Options"; import { LoaderFactory } from "./Loaders/LoaderFactory"; import { MarkupFactory } from "./Markup/MarkupFactory"; import { IMarkup, MarkupType } from "./Markup/IMarkup"; const OVERLAY_VIEW_NAME = "$OVERLAY_VIEW_NAME"; const isExist = (value) => value !== undefined && value !== null; /** * The `Client.js` library class that provides methods to integrate with the * [VisualizeJS](https://cloud.opendesign.com/docs/index.html#/visualizejs) library. */ export class Viewer extends EventEmitter2 implements IViewer { private _activeDragger: OdBaseDragger | null; private _zoomWheelDragger: OdZoomWheelDragger | null; private _gestureManager: GestureManager | null; private _enableAutoUpdate: boolean; private _isNeedRender: boolean; private _isRunAsyncUpdate: boolean; private _renderTime: DOMHighResTimeStamp; protected _options: Options; protected _visualizeJsUrl: string; protected _abortControllerForReferences: AbortController | undefined; private clientoptionschange: (event: OptionsChangeEvent) => void; private canvaseventlistener: (event: Event) => void; public draggerFactory: Map; public canvasEvents: string[]; private frameId: number; private _resizeObserver: ResizeObserver | undefined; public canvas: HTMLCanvasElement | undefined; public markup: IMarkup; public visualizeJs: any; public _abortController: AbortController | undefined; public _abortControllerForRequestMap: Map | undefined; public client: Client; /** * @param client - The `Client` instance that provides access to a server. Do not specify * `Client` if you need a standalone viewer instance without access to server models. * @param params - An object containing viewer configuration parameters. * @param params.visualizeJsUrl - `VisualizeJS` library URL. Set this URL to use your own * library instance, or leave it undefined or blank to use the default URL defined by * `Client.js` you are using. * * _Note: Your own `VisualizeJS` library version must match the version of the `Client.js` * you are using._ * @param params.enableAutoUpdate - Enable auto-update of the viewer after any changes. If * the auto-update is disabled, you need to update the `VisualizeJS` viewer and the active * dragger manually using the `update` event. Default is `true`. */ constructor( client?: Client, params: { visualizeJsUrl?: string; enableAutoUpdate?: boolean; markupType?: MarkupType } = { markupType: MarkupType.Konva, } ) { super(); this.configure(params); this._options = new Options(this); this.client = client; this.clientoptionschange = (event: OptionsChangeEvent) => (this._options.data = event.data.data); this._activeDragger = null; this._renderTime = 0; this.markup = MarkupFactory.createMarkup(params.markupType ?? MarkupType.Konva); this.draggerFactory = new Map(); this.registerDragger("Pan", OdPanDragger); this.registerDragger("Orbit", OdOrbitDragger); this.registerDragger("Zoom", OdZoomDragger); this.registerDragger("ZoomWindow", OdZoomWindowDragger); this.registerDragger("OrbitAroundBuilding", OrbitAroundBuildingDragger); this.registerDragger("MeasureLine", MeasureLineDragger); this.registerDragger("CuttingPlaneXAxis", OdCuttingPlaneXAxisDragger); this.registerDragger("CuttingPlaneYAxis", OdCuttingPlaneYAxisDragger); this.registerDragger("CuttingPlaneZAxis", OdCuttingPlaneZAxisDragger); this.registerDragger("Walk", OdaWalkDragger); const markupDraggers = this.markup.getDraggers(); markupDraggers?.forEach((value, key) => { this.registerDragger(key, value); }); this.canvasEvents = CANVAS_EVENTS; 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.resize = this.resize.bind(this); } /** * `VisualizeJS` parameters. * * Use this only for standalone viewer instances. Use {@link Client#options | Client.options} * instead if the viewer was created with a `Client` reference using * {@link Viewer | Viewer.create(client)}. Changes in client options will be automatically * applied to the viewer options. */ get options(): Options { return this._options; } /** * `VisualizeJS` library URL. Use {@link Viewer#configure | configure()} to change library URL. * * @readonly */ get visualizeJsUrl(): string { return this._visualizeJsUrl; } /** * Change the viewer configuration parameters. * * @param params - An object containing new configuration parameters. * @param params.visualizeJsUrl - `VisualizeJS` library URL. Set this URL to use your own * library instance or leave it blank to use the default URL defined by `Client.js`. */ configure(params: { visualizeJsUrl?: string }): this { this._visualizeJsUrl = params.visualizeJsUrl || "VISUALIZE_JS_URL"; return this; } /** * A [Viewer]{@link Viewer} event measuring progress of loading a `VisualizeJS` library. * * @property {string} type - `visualizeprogress` * @property {number} loaded - A 64-bit unsigned integer value indicating the amount of work * already performed by the underlying process. The ratio of work done can be calculated by * dividing total by the value of this property. * @property {number} total - A 64-bit unsigned integer representing the total amount of work * that the underlying process is in the progress of performing. * @event visualizeprogress */ /** * Load `VisualizeJS` module and initialize it with the specified canvas. Call * {@link Viewer#dispose | dispose()} to release allocated resources. * * @async * @param canvas - HTML `` element for `VisualizeJS`. * @param onProgress - A callback function that handles events measuring progress of loading * of the `VisualizeJS` library. Retrieves {@link event:visualizeprogress | visualizeprogress} event. */ async 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)); if (canvas.style.width === "" && canvas.style.height === "") { canvas.style.width = "100%"; canvas.style.height = "100%"; } canvas.style.touchAction = "none"; canvas.width = canvas.clientWidth * window.devicePixelRatio; canvas.height = canvas.clientHeight * window.devicePixelRatio; const visualizeJs: any = await loadVisualizeJs(this.visualizeJsUrl, (ev) => { const { loaded, timeStamp, total, lengthComputable } = ev; const event = { loaded, timeStamp, total, lengthComputable, type: "visualizeprogress" } as ProgressEvent; onProgress?.(event); }); this.visualizeJs = visualizeJs; this.visualizeJs.canvas = canvas; this.visualizeJs.Viewer.create(); this.canvas = canvas; this.canvasEvents.forEach((x) => canvas.addEventListener(x, this.canvaseventlistener)); this.markup.initialize(this, canvas, this.canvasEvents); this._resizeObserver = new ResizeObserver(this.resize); this._resizeObserver.observe(canvas.parentElement); this.resize(); this._zoomWheelDragger = new OdZoomWheelDragger(this); this._zoomWheelDragger.name = "ZoomWheel"; this._zoomWheelDragger.initialize(); this._gestureManager = new GestureManager(this); this._gestureManager.initialize(); this.syncOpenCloudVisualStyle(true); this.syncOptions(); this._renderTime = performance.now(); this.render(this._renderTime); return this; } /** * Releases all resources allocated by this `Viewer` instance. Call this method before * release the `Viewer` instance. */ dispose(): this { this.cancel(); this.emitEvent({ type: "dispose" }); if (this.frameId) cancelAnimationFrame(this.frameId); this.frameId = 0; this.setActiveDragger(""); this._zoomWheelDragger?.dispose(); this._gestureManager?.dispose(); this.removeAllListeners(); this._resizeObserver?.disconnect(); this._resizeObserver = undefined; this.markup.dispose(); this.canvasEvents.forEach((x) => this.canvas?.removeEventListener(x, this.canvaseventlistener)); this.canvas = undefined; this.visualizeJs?.getViewer().clear(); this.visualizeJs = undefined; this.client?.removeEventListener("optionschange", this.clientoptionschange); return this; } /** * Returns `true` if `VisualizeJS` module has been loaded andinitialized. */ isInitialized(): boolean { return !!this.visualizeJs; } // internal render/resize routines private render(time: DOMHighResTimeStamp) { this.frameId = requestAnimationFrame(this.render); 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; 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 (use * the `update` event to update viewer manually). * * Fires: * * - {@link UpdateEvent | update} * * @param force - If `true` updates the viewer immidietly. Otherwise updates on next * animation frame. Default is `false`. */ update(force?: boolean) { if (this._enableAutoUpdate) { if (force) { this.visViewer()?.update(); this.activeDragger()?.updatePreview(); } else { this._isNeedRender = true; } } this.emitEvent({ type: "update", data: force }); } /** * Update with internal schedule, need after change operation when have long update for * update without lock UI * * @param maxScheduleUpdateTimeInMs - Maximum time for one update, by default 30 ms * @param maxScheduleUpdateCount - Maximum count of schedule update * @returns return void Promise */ private scheduleUpdateAsync(maxScheduleUpdateTimeInMs = 50): Promise { return new Promise((resolve, reject) => { setTimeout(() => { try { this.visViewer()?.update(maxScheduleUpdateTimeInMs); this.activeDragger()?.updatePreview(); resolve(); } catch (e) { console.error(e); reject(); } }, 0); }); } async updateAsync(maxScheduleUpdateTimeInMs = 50, maxScheduleUpdateCount = 50): Promise { 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. */ 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(); // setup light view.enableDefaultLighting(true, visLib.DefaultLightingType.kTwoLights); view.setDefaultLightingIntensity(1.25); let visualStyleId; try { visualStyleId = visViewer.findVisualStyle("OpenCloud"); } catch (e) { if (!isInitializing) { console.log("OpenCloud visual style not found, creating it on client side"); } 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, 1, 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: OptionsData = this.options.data): 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); } if (options.antialiasing !== visViewer.fxaaAntiAliasing3d) { visViewer.fxaaAntiAliasing3d = options.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: OptionsData = this.options.data): 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; } /** * List of names of available draggers: * * - `Line` * - `Text` * - `Pan` * - `Orbit` * - `Zoom` * - `ZoomWindow` * - `OrbitAroundBuilding` * - `MeasureLine` * - `CuttingPlaneXAxis` * - `CuttingPlaneYAxis` * - `CuttingPlaneZAxis` * - `Walk` * * @readonly */ get draggers(): string[] { return [...this.draggerFactory.keys()]; } /** * Register dragger on draggerFactory. * * @param name - Dragger name. * @param dragger - Dragger class. */ public registerDragger(name: string, dragger: typeof OdBaseDragger): void { this.draggerFactory.set(name, dragger); } /** * Returns active dragger instance or `null` if there is no active dragger. */ activeDragger(): any | null { return this._activeDragger; } /** * Set active dragger. `Viewer` must be {@link Viewer#initialize | initialized} before enable * dragger or exception is thrown. * * Fires: * * - {@link ChangeActiveDragger | changeactivedragger} * * @param name - Dragger name. Can be one of the {@link Viewer#draggers | draggers} list. * @returns Returns active dragger instance or `null` if there is no dragger with the given name. */ setActiveDragger(name: string): OdBaseDragger | null { if (this._activeDragger?.name !== name) { if (this._activeDragger) { this._activeDragger.dispose(); this._activeDragger = null; } const Constructor = this.draggerFactory.get(name); if (Constructor) { this._activeDragger = new Constructor(this); this._activeDragger.name = name; this._activeDragger.initialize(); } const canvas = this.canvas; if (canvas) { canvas.className = canvas.className .split(" ") .filter((x) => !x.startsWith("oda-cursor-")) .filter((x) => x) .concat(`oda-cursor-${name.toLowerCase()}`) .join(" "); } this.emitEvent({ type: "changeactivedragger", data: name }); } return this._activeDragger; } /** * Reset the state of the active dragger. */ resetActiveDragger(): void { const dragger = this._activeDragger; if (dragger) { this.setActiveDragger(""); this.setActiveDragger(dragger.name); } } /** * Remove all cutting planes. */ clearSlices(): void { if (!this.visualizeJs) return; const visViewer = this.visViewer(); const activeView = visViewer.activeView; activeView.removeCuttingPlanes(); activeView.delete(); this.update(); } /** * Remove markup overlay. */ clearOverlay(): void { this.markup.clearOverlay(); } /** * Synchronize markup overlay. */ syncOverlay(): any { if (!this.visualizeJs) return; const visViewer = this.visViewer(); const activeView = visViewer.activeView; let overlayView = visViewer.getViewByName(OVERLAY_VIEW_NAME); if (!overlayView) { const overlayModel = visViewer.getMarkupModel(); const pDevice = visViewer.getActiveDevice(); overlayView = pDevice.createView(OVERLAY_VIEW_NAME, false); overlayView.addModel(overlayModel); 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.update(); return overlayView; } /** * Returns `true` if current drawing is 3D drawing. */ 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; } /** * Returns a list of original handles for the selected entities. */ getSelected(): string[] { return this.executeCommand("getSelected"); } /** * Select model entities by original handles that are obtained using * {@link File.getProperties | File.getProperties()} or * {@link File.searchProperties | File.searchProperties()} methods. * * Fires: * * - {@link SelectEvent | select} * * @param handles - The list of original handles. */ setSelected(handles?: string[]): void { this.executeCommand("setSelected", handles); } /** * Load model references into the viewer. References are images, fonts, or any other files to * correct rendering of the model. * * @async * @param model - Instance of model with references. If a `File` instance is specified * instead of a model, the file references will be loaded. */ async loadReferences(model: Model | File | Assembly): Promise { if (!this.visualizeJs) return this; const abortController = new AbortController(); this._abortControllerForReferences?.abort(); this._abortControllerForReferences = abortController; let references = []; 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; } // Internal loading routines 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 model of a file or assembly into the viewer. * * This method requires a [Client]{@link Client | Client} instance to work. For standalone * viewer instance use {@link Viewer#openVsfFile | openVsfFile()} or * {@link Viewer#openVsfxFile | openVsfxFile()}. * * Fires: * * - {@link OpenEvent | open} * - {@link GeometryStartEvent | geometrystart} * - {@link GeometryProgressEvent | geometryprogress} * - {@link DatabaseChunkEvent | databasechunk} * - {@link GeometryChunkEvent | geometrychunk} * - {@link GeometryEndEvent | geometryend} * - {@link GeometryErrorEvent | geometryerror} * * @async * @param model - Model instance to load. If a `File` instance with multiple models is * specified, the default model will be loaded. If there is no default model, first * availiable model will be loaded. */ async open(model: Model | File | Assembly): Promise { if (!this.visualizeJs) return this; this.cancel(); this.clear(); this.emitEvent({ type: "open", model }); if (model instanceof File || model instanceof Assembly) { const models = (await model.getModels()) ?? []; model = models.find((model) => model.default) ?? models[0]; } if (!model) throw new Error("No default model found"); await this.loadReferences(model); const overrideOptions = new Options(); overrideOptions.data = this.options.data; if (model.file.type === ".rcs") { console.log("Partial load mode is forced for RCS file"); overrideOptions.enablePartialMode = true; } const loaderFactory = new LoaderFactory(); const loader = loaderFactory.create(this, model, overrideOptions.data); await loader.load(); if (this.visualizeJs) { this.applyModelTransformMatrix(model); this.applySceneGraphSettings(); } return this; } /** * Loads a VSF file into the viewer. * * Fires: * * - {@link OpenEvent | open} * - {@link GeometryStartEvent | geometrystart} * - {@link GeometryProgressEvent | geometryprogress} * - {@link DatabaseChunkEvent | databasechunk} * - {@link GeometryEndEvent | geometryend} * - {@link GeometryErrorEvent | geometryerror} * * @param buffer - Binary data buffer to load. */ openVsfFile(buffer: Uint8Array | ArrayBuffer): this { if (!this.visualizeJs) return this; this.cancel(); this.clear(); this.emitEvent({ type: "open", buffer }); try { this.emitEvent({ type: "geometrystart", buffer }); const visLib = this.visLib(); const visViewer = visLib.getViewer(); const data = buffer instanceof Uint8Array ? buffer : new Uint8Array(buffer); visViewer.parseFile(data); this.syncOpenCloudVisualStyle(false); this.syncOptions(); this.resize(); this.emitEvent({ type: "geometryprogress", data: 1, buffer }); this.emitEvent({ type: "databasechunk", data, buffer }); this.emitEvent({ type: "geometryend", buffer }); } catch (error) { this.emitEvent({ type: "geometryerror", data: error, buffer }); throw error; } return this; } /** * Loads a VSFX file into the viewer. * * Fires: * * - {@link OpenEvent | open} * - {@link GeometryStartEvent | geometrystart} * - {@link GeometryProgressEvent | geometryprogress} * - {@link DatabaseChunkEvent | databasechunk} * - {@link GeometryEndEvent | geometryend} * - {@link GeometryErrorEvent | geometryerror} * * @param buffer - Binary data buffer to load. */ openVsfxFile(buffer: Uint8Array | ArrayBuffer): this { if (!this.visualizeJs) return this; this.cancel(); this.clear(); this.emitEvent({ type: "open", buffer }); try { this.emitEvent({ type: "geometrystart", buffer }); const visLib = this.visLib(); const visViewer = visLib.getViewer(); const data = buffer instanceof Uint8Array ? buffer : new Uint8Array(buffer); visViewer.parseVsfx(data); this.syncOpenCloudVisualStyle(false); this.syncOptions(); this.resize(); this.emitEvent({ type: "geometryprogress", data: 1, buffer }); this.emitEvent({ type: "databasechunk", data, buffer }); this.emitEvent({ type: "geometryend", buffer }); } catch (error) { this.emitEvent({ type: "geometryerror", data: error, buffer }); throw error; } return this; } /** * Cancels asynchronous model loading started by {@link Viewer#open | open()}. */ cancel(): this { this._abortControllerForReferences?.abort(); this._abortControllerForReferences = undefined; this._abortController?.abort(); this._abortController = undefined; this._abortControllerForRequestMap?.forEach((controller) => controller.abort()); this._abortControllerForRequestMap = undefined; this.emitEvent({ type: "cancel" }); return this; } /** * Unloads the model and clears the viewer. */ clear(): this { if (!this.visualizeJs) return this; const visLib = this.visLib(); const visViewer = visLib.getViewer(); this.clearOverlay(); visViewer.clear(); visViewer.createLocalDatabase(); this.syncOpenCloudVisualStyle(true); this.syncOptions(); this.resize(); this.emitEvent({ type: "clear" }); return this; } /** * Get markup color. * * @returns Color with `RGB` values. */ getMarkupColor(): { r: number; g: number; b: number } { return this.markup.getMarkupColor(); } /** * Set markup color. * * @param r - `Red` part of color. * @param g - `Green` part of color. * @param b - `Blue` part of color. */ setMarkupColor(r = 255, g = 0, b = 0): void { this.markup.setMarkupColor(r, g, b); const color = { r, g, b }; this.emitEvent({ type: "changemarkupcolor", data: color }); } /** * Colorize all markup entities with the specified color. * * @param r - `Red` part of color. * @param g - `Green` part of color. * @param b - `Blue` part of color. */ colorizeAllMarkup(r = 255, g = 0, b = 0): void { this.markup.colorizeAllMarkup(r, g, b); } /** * Add an empty 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; } /** * Draw a viewpoint. To get a list of available model viewpoints, use the * {@link Model#getViewpoints | Model.getViewpoints()} or * {@link File#getViewpoints | File.getViewpoints()}. * * @param viewpoint - Viewpoint. */ drawViewpoint(viewpoint: any): void { this.markup.drawViewpoint(viewpoint); } /** * Create a viewpoint. To add a viewpoint to the list of model viewpoints, use the * {@link Model#saveViewpoint | Model.saveViewpoint()} or * {@link File#saveViewpoint | File.saveViewpoint()}. */ createViewpoint(): object { const vp = this.markup.createViewpoint(); return vp; } /** * Executes the command denoted by the given command identifier. * * @param id - Identifier of the command to execute. * @param args - Parameters passed to the command function. * @returns A returned value of the given command. Returns `undefined` when the command doesn't exists. */ executeCommand(id: string, ...args: any[]): any { return commands("VisualizeJS").executeCommand(id, this, ...args); } }