@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
text/typescript
///////////////////////////////////////////////////////////////////////////////
// Copyright (C) 2002-2025, Open Design Alliance (the "Alliance").
// All rights reserved.
//
// This software and its documentation and related materials are owned by
// the Alliance. The software may only be incorporated into application
// programs owned by members of the Alliance, subject to a signed
// Membership Agreement and Supplemental Software License Agreement with the
// Alliance. The structure and organization of this software are the valuable
// trade secrets of the Alliance and its suppliers. The software is also
// protected by copyright law and international treaty provisions. Application
// programs incorporating this software must include the following statement
// with their copyright notices:
//
// This application incorporates Open Design Alliance software pursuant to a
// license agreement with Open Design Alliance.
// Open Design Alliance Copyright (C) 2002-2025 by Open Design Alliance.
// All rights reserved.
//
// By use of this software, its documentation or related materials, you
// acknowledge and accept the above terms.
///////////////////////////////////////////////////////////////////////////////
import { 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();
}
}
}