@inweb/viewer-three
Version:
JavaScript library for rendering CAD and BIM files in a browser using Three.js
1,215 lines (1,203 loc) • 290 kB
JavaScript
///////////////////////////////////////////////////////////////////////////////
// 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 { draggersRegistry, commandsRegistry, Options, componentsRegistry, Info, Loader, loadersRegistry, CANVAS_EVENTS } from '@inweb/viewer-core';
export * from '@inweb/viewer-core';
import { Line, Vector3, BufferGeometry, Float32BufferAttribute, LineBasicMaterial, Mesh, MeshBasicMaterial, DoubleSide, EventDispatcher, MOUSE, TOUCH, Spherical, Quaternion, Vector2, Plane, Object3D, Line3, Raycaster, MathUtils, EdgesGeometry, Matrix4, Vector4, Controls, Clock, Sphere, Box3, Color, PerspectiveCamera, OrthographicCamera, AmbientLight, DirectionalLight, HemisphereLight, REVISION, MeshPhongMaterial, WebGLRenderTarget, UnsignedByteType, RGBAFormat, CylinderGeometry, Sprite, CanvasTexture, SRGBColorSpace, SpriteMaterial, TextureLoader, BufferAttribute, PointsMaterial, Points, TriangleStripDrawMode, TriangleFanDrawMode, LineSegments, LineLoop, Group, NormalBlending, LoadingManager, LoaderUtils, FileLoader, UniformsUtils, ShaderMaterial, AdditiveBlending, HalfFloatType, Scene, WebGLRenderer, LinearSRGBColorSpace } from 'three';
import { TransformControls } from 'three/examples/jsm/controls/TransformControls.js';
import { LineSegmentsGeometry } from 'three/examples/jsm/lines/LineSegmentsGeometry.js';
import { Wireframe } from 'three/examples/jsm/lines/Wireframe.js';
import { LineMaterial } from 'three/examples/jsm/lines/LineMaterial.js';
import { mergeGeometries } from 'three/examples/jsm/utils/BufferGeometryUtils.js';
import { EffectComposer } from 'three/examples/jsm/postprocessing/EffectComposer.js';
import { RenderPass } from 'three/examples/jsm/postprocessing/RenderPass.js';
import { FXAAPass } from 'three/examples/jsm/postprocessing/FXAAPass.js';
import { SMAAPass } from 'three/examples/jsm/postprocessing/SMAAPass.js';
import { Pass, FullScreenQuad } from 'three/examples/jsm/postprocessing/Pass.js';
import { CopyShader } from 'three/examples/jsm/shaders/CopyShader.js';
import { OutputPass } from 'three/examples/jsm/postprocessing/OutputPass.js';
import { EventEmitter2 } from '@inweb/eventemitter2';
import { Markup } from '@inweb/markup';
export * from '@inweb/markup';
class PlaneHelper extends Line {
constructor(plane, size = 1, color = 0xffff00, offset = new Vector3()) {
const positions = [1, 1, 0, -1, 1, 0, -1, -1, 0, 1, -1, 0, 1, 1, 0];
const geometry = new BufferGeometry();
geometry.setAttribute("position", new Float32BufferAttribute(positions, 3));
geometry.computeBoundingSphere();
super(geometry, new LineBasicMaterial({ color, toneMapped: false }));
this.type = "PlaneHelper";
this.plane = plane;
this.size = size;
this.offset = offset;
const positions2 = [1, 1, 0, -1, 1, 0, -1, -1, 0, 1, 1, 0, -1, -1, 0, 1, -1, 0];
const geometry2 = new BufferGeometry();
geometry2.setAttribute("position", new Float32BufferAttribute(positions2, 3));
geometry2.computeBoundingSphere();
this.helper = new Mesh(geometry2, new MeshBasicMaterial({
color,
opacity: 0.2,
transparent: true,
depthWrite: false,
toneMapped: false,
side: DoubleSide,
}));
this.add(this.helper);
}
dispose() {
this.geometry.dispose();
this.material.dispose();
this.children[0].geometry.dispose();
this.children[0].material.dispose();
}
updateMatrixWorld(force) {
this.position.set(0, 0, 0);
this.lookAt(this.plane.normal);
this.position.copy(this.offset);
this.translateZ(-(this.offset.dot(this.plane.normal) + this.plane.constant));
this.scale.set(0.5 * this.size, 0.5 * this.size, 1);
super.updateMatrixWorld(force);
}
}
const _changeEvent = { type: "change" };
const _startEvent = { type: "start" };
const _endEvent = { type: "end" };
const STATE = {
NONE: -1,
ROTATE: 0,
DOLLY: 1,
PAN: 2,
TOUCH_ROTATE: 3,
TOUCH_PAN: 4,
TOUCH_DOLLY_PAN: 5,
TOUCH_DOLLY_ROTATE: 6,
};
class OrbitControls extends EventDispatcher {
constructor(object, domElement) {
super();
this.object = object;
this.domElement = domElement;
this.domElement.style.touchAction = "none";
this.enabled = true;
this.target = new Vector3();
this.minDistance = 0;
this.maxDistance = Infinity;
this.minZoom = 0;
this.maxZoom = Infinity;
this.minPolarAngle = 0;
this.maxPolarAngle = Math.PI;
this.minAzimuthAngle = -Infinity;
this.maxAzimuthAngle = Infinity;
this.enableDamping = false;
this.dampingFactor = 0.05;
this.enableZoom = true;
this.zoomSpeed = 1.0;
this.enableRotate = true;
this.rotateSpeed = 1.0;
this.enablePan = true;
this.panSpeed = 1.0;
this.screenSpacePanning = true;
this.keyPanSpeed = 7.0;
this.autoRotate = false;
this.autoRotateSpeed = 2.0;
this.keys = { LEFT: "ArrowLeft", UP: "ArrowUp", RIGHT: "ArrowRight", BOTTOM: "ArrowDown" };
this.mouseButtons = { LEFT: MOUSE.ROTATE, MIDDLE: MOUSE.DOLLY, RIGHT: MOUSE.PAN };
this.touches = { ONE: TOUCH.ROTATE, TWO: TOUCH.DOLLY_PAN };
this.target0 = this.target.clone();
this.position0 = this.object.position.clone();
this.zoom0 = this.object.zoom;
this._domElementKeyEvents = null;
this.getPolarAngle = function () {
return spherical.phi;
};
this.getAzimuthalAngle = function () {
return spherical.theta;
};
this.getDistance = function () {
return this.object.position.distanceTo(this.target);
};
this.listenToKeyEvents = function (domElement) {
domElement.addEventListener("keydown", onKeyDown);
this._domElementKeyEvents = domElement;
};
this.stopListenToKeyEvents = function () {
this._domElementKeyEvents.removeEventListener("keydown", onKeyDown);
this._domElementKeyEvents = null;
};
this.saveState = function () {
scope.target0.copy(scope.target);
scope.position0.copy(scope.object.position);
scope.zoom0 = scope.object.zoom;
};
this.reset = function () {
scope.target.copy(scope.target0);
scope.object.position.copy(scope.position0);
scope.object.zoom = scope.zoom0;
scope.object.updateProjectionMatrix();
scope.dispatchEvent(_changeEvent);
scope.update();
scope.state = STATE.NONE;
};
this.update = (function () {
const offset = new Vector3();
const quat = new Quaternion().setFromUnitVectors(object.up, new Vector3(0, 1, 0));
const quatInverse = quat.clone().invert();
const lastPosition = new Vector3();
const lastQuaternion = new Quaternion();
const lastTargetPosition = new Vector3();
const twoPI = 2 * Math.PI;
return function update() {
const position = scope.object.position;
offset.copy(position).sub(scope.target);
offset.applyQuaternion(quat);
spherical.setFromVector3(offset);
if (scope.autoRotate && scope.state === STATE.NONE) {
rotateLeft(getAutoRotationAngle());
}
if (scope.enableDamping) {
spherical.theta += sphericalDelta.theta * scope.dampingFactor;
spherical.phi += sphericalDelta.phi * scope.dampingFactor;
} else {
spherical.theta += sphericalDelta.theta;
spherical.phi += sphericalDelta.phi;
}
let min = scope.minAzimuthAngle;
let max = scope.maxAzimuthAngle;
if (isFinite(min) && isFinite(max)) {
if (min < -Math.PI) min += twoPI;
else if (min > Math.PI) min -= twoPI;
if (max < -Math.PI) max += twoPI;
else if (max > Math.PI) max -= twoPI;
if (min <= max) {
spherical.theta = Math.max(min, Math.min(max, spherical.theta));
} else {
spherical.theta =
spherical.theta > (min + max) / 2 ? Math.max(min, spherical.theta) : Math.min(max, spherical.theta);
}
}
spherical.phi = Math.max(scope.minPolarAngle, Math.min(scope.maxPolarAngle, spherical.phi));
spherical.makeSafe();
spherical.radius *= scope.scale;
spherical.radius = Math.max(scope.minDistance, Math.min(scope.maxDistance, spherical.radius));
if (scope.enableDamping === true) {
scope.target.addScaledVector(scope.panOffset, scope.dampingFactor);
} else {
scope.target.add(scope.panOffset);
}
offset.setFromSpherical(spherical);
offset.applyQuaternion(quatInverse);
position.copy(scope.target).add(offset);
scope.object.lookAt(scope.target);
if (scope.enableDamping === true) {
sphericalDelta.theta *= 1 - scope.dampingFactor;
sphericalDelta.phi *= 1 - scope.dampingFactor;
scope.panOffset.multiplyScalar(1 - scope.dampingFactor);
} else {
sphericalDelta.set(0, 0, 0);
scope.panOffset.set(0, 0, 0);
}
scope.scale = 1;
if (
scope.zoomChanged ||
lastPosition.distanceToSquared(scope.object.position) > EPS ||
8 * (1 - lastQuaternion.dot(scope.object.quaternion)) > EPS ||
lastTargetPosition.distanceToSquared(scope.target) > 0
) {
scope.dispatchEvent(_changeEvent);
lastPosition.copy(scope.object.position);
lastQuaternion.copy(scope.object.quaternion);
lastTargetPosition.copy(scope.target);
scope.zoomChanged = false;
return true;
}
return false;
};
})();
this.dispose = function () {
scope.domElement.removeEventListener("contextmenu", onContextMenu);
scope.domElement.removeEventListener("pointerdown", onPointerDown);
scope.domElement.removeEventListener("pointercancel", onPointerUp);
scope.domElement.removeEventListener("wheel", onMouseWheel);
scope.domElement.removeEventListener("pointermove", onPointerMove);
scope.domElement.removeEventListener("pointerup", onPointerUp);
if (scope._domElementKeyEvents !== null) {
scope._domElementKeyEvents.removeEventListener("keydown", onKeyDown);
scope._domElementKeyEvents = null;
}
};
const scope = this;
scope.state = STATE.NONE;
const EPS = 0.000001;
const spherical = new Spherical();
const sphericalDelta = new Spherical();
scope.scale = 1;
scope.panOffset = new Vector3();
scope.zoomChanged = false;
scope.rotateStart = new Vector2();
scope.rotateEnd = new Vector2();
scope.rotateDelta = new Vector2();
scope.panStart = new Vector2();
scope.panEnd = new Vector2();
scope.panDelta = new Vector2();
scope.dollyStart = new Vector2();
scope.dollyEnd = new Vector2();
scope.dollyDelta = new Vector2();
scope.dollyScale = 0;
scope.pointers = [];
scope.pointerPositions = {};
function getAutoRotationAngle() {
return ((2 * Math.PI) / 60 / 60) * scope.autoRotateSpeed;
}
function getZoomScale() {
return Math.pow(0.95, scope.zoomSpeed);
}
function rotateLeft(angle) {
sphericalDelta.theta -= angle;
}
function rotateUp(angle) {
sphericalDelta.phi -= angle;
}
const panLeft = (function () {
const v = new Vector3();
return function panLeft(distance, objectMatrix) {
v.setFromMatrixColumn(objectMatrix, 0);
v.multiplyScalar(-distance);
scope.panOffset.add(v);
};
})();
const panUp = (function () {
const v = new Vector3();
return function panUp(distance, objectMatrix) {
if (scope.screenSpacePanning === true) {
v.setFromMatrixColumn(objectMatrix, 1);
} else {
v.setFromMatrixColumn(objectMatrix, 0);
v.crossVectors(scope.object.up, v);
}
v.multiplyScalar(distance);
scope.panOffset.add(v);
};
})();
const pan = (function () {
const offset = new Vector3();
return function pan(deltaX, deltaY) {
const element = scope.domElement;
if (scope.object.isPerspectiveCamera) {
const position = scope.object.position;
offset.copy(position).sub(scope.target);
let targetDistance = offset.length();
targetDistance *= Math.tan(((scope.object.fov / 2) * Math.PI) / 180.0);
panLeft((2 * deltaX * targetDistance) / element.clientHeight, scope.object.matrix);
panUp((2 * deltaY * targetDistance) / element.clientHeight, scope.object.matrix);
} else if (scope.object.isOrthographicCamera) {
panLeft(
(deltaX * (scope.object.right - scope.object.left)) / scope.object.zoom / element.clientWidth,
scope.object.matrix
);
panUp(
(deltaY * (scope.object.top - scope.object.bottom)) / scope.object.zoom / element.clientHeight,
scope.object.matrix
);
} else {
console.warn("WARNING: OrbitControls.js encountered an unknown camera type - pan disabled.");
scope.enablePan = false;
}
};
})();
function dollyOut(dollyScale) {
if (scope.object.isPerspectiveCamera) {
scope.scale /= dollyScale;
} else if (scope.object.isOrthographicCamera) {
scope.object.zoom = Math.max(scope.minZoom, Math.min(scope.maxZoom, scope.object.zoom * dollyScale));
scope.object.updateProjectionMatrix();
scope.zoomChanged = true;
} else {
console.warn("WARNING: OrbitControls.js encountered an unknown camera type - dolly/zoom disabled.");
scope.enableZoom = false;
}
}
function dollyIn(dollyScale) {
if (scope.object.isPerspectiveCamera) {
scope.scale *= dollyScale;
} else if (scope.object.isOrthographicCamera) {
scope.object.zoom = Math.max(scope.minZoom, Math.min(scope.maxZoom, scope.object.zoom / dollyScale));
scope.object.updateProjectionMatrix();
scope.zoomChanged = true;
} else {
console.warn("WARNING: OrbitControls.js encountered an unknown camera type - dolly/zoom disabled.");
scope.enableZoom = false;
}
}
function handleMouseDownRotate(event) {
scope.rotateStart.set(event.clientX, event.clientY);
}
function handleMouseDownDolly(event) {
scope.dollyStart.set(event.clientX, event.clientY);
}
function handleMouseDownPan(event) {
scope.panStart.set(event.clientX, event.clientY);
}
function handleMouseMoveRotate(event) {
scope.rotateEnd.set(event.clientX, event.clientY);
scope.rotateDelta.subVectors(scope.rotateEnd, scope.rotateStart).multiplyScalar(scope.rotateSpeed);
const element = scope.domElement;
rotateLeft((2 * Math.PI * scope.rotateDelta.x) / element.clientHeight);
rotateUp((2 * Math.PI * scope.rotateDelta.y) / element.clientHeight);
scope.rotateStart.copy(scope.rotateEnd);
scope.update();
}
function handleMouseMoveDolly(event) {
scope.dollyEnd.set(event.clientX, event.clientY);
scope.dollyDelta.subVectors(scope.dollyEnd, scope.dollyStart);
if (scope.dollyDelta.y < 0) {
scope.dollyScale = 1 / getZoomScale();
dollyOut(getZoomScale());
} else if (scope.dollyDelta.y > 0) {
scope.dollyScale = getZoomScale();
dollyIn(getZoomScale());
}
scope.dollyStart.copy(scope.dollyEnd);
scope.update();
}
function handleMouseMovePan(event) {
scope.panEnd.set(event.clientX, event.clientY);
scope.panDelta.subVectors(scope.panEnd, scope.panStart).multiplyScalar(scope.panSpeed);
pan(scope.panDelta.x, scope.panDelta.y);
scope.panStart.copy(scope.panEnd);
scope.update();
}
function handleMouseWheel(event) {
scope.dollyEnd.set(scope.domElement.clientWidth / 2, scope.domElement.clientHeight / 2);
scope.dollyDelta.set(event.deltaX, event.deltaY);
if (event.deltaY < 0) {
scope.dollyScale = 1 / getZoomScale();
dollyIn(getZoomScale());
} else if (event.deltaY > 0) {
scope.dollyScale = getZoomScale();
dollyOut(getZoomScale());
}
scope.dollyStart.copy(scope.dollyEnd);
scope.update();
if (event.deltaY !== 0) {
scope.state = STATE.DOLLY;
scope.dispatchEvent(_changeEvent);
scope.state = STATE.NONE;
}
}
function handleKeyDown(event) {
let needsUpdate = false;
switch (event.code) {
case scope.keys.UP:
if (event.ctrlKey || event.metaKey || event.shiftKey) {
rotateUp((2 * Math.PI * scope.rotateSpeed) / scope.domElement.clientHeight);
} else {
pan(0, scope.keyPanSpeed);
}
needsUpdate = true;
break;
case scope.keys.BOTTOM:
if (event.ctrlKey || event.metaKey || event.shiftKey) {
rotateUp((-2 * Math.PI * scope.rotateSpeed) / scope.domElement.clientHeight);
} else {
pan(0, -scope.keyPanSpeed);
}
needsUpdate = true;
break;
case scope.keys.LEFT:
if (event.ctrlKey || event.metaKey || event.shiftKey) {
rotateLeft((2 * Math.PI * scope.rotateSpeed) / scope.domElement.clientHeight);
} else {
pan(scope.keyPanSpeed, 0);
}
needsUpdate = true;
break;
case scope.keys.RIGHT:
if (event.ctrlKey || event.metaKey || event.shiftKey) {
rotateLeft((-2 * Math.PI * scope.rotateSpeed) / scope.domElement.clientHeight);
} else {
pan(-scope.keyPanSpeed, 0);
}
needsUpdate = true;
break;
}
if (needsUpdate) {
event.preventDefault();
scope.update();
}
}
function handleTouchStartRotate() {
if (scope.pointers.length === 1) {
scope.rotateStart.set(scope.pointers[0].pageX, scope.pointers[0].pageY);
} else {
const x = 0.5 * (scope.pointers[0].pageX + scope.pointers[1].pageX);
const y = 0.5 * (scope.pointers[0].pageY + scope.pointers[1].pageY);
scope.rotateStart.set(x, y);
}
}
function handleTouchStartPan() {
if (scope.pointers.length === 1) {
scope.panStart.set(scope.pointers[0].pageX, scope.pointers[0].pageY);
} else {
const x = 0.5 * (scope.pointers[0].pageX + scope.pointers[1].pageX);
const y = 0.5 * (scope.pointers[0].pageY + scope.pointers[1].pageY);
scope.panStart.set(x, y);
}
}
function handleTouchStartDolly() {
const dx = scope.pointers[0].pageX - scope.pointers[1].pageX;
const dy = scope.pointers[0].pageY - scope.pointers[1].pageY;
const distance = Math.sqrt(dx * dx + dy * dy);
scope.dollyStart.set(0, distance);
}
function handleTouchStartDollyPan() {
if (scope.enableZoom) handleTouchStartDolly();
if (scope.enablePan) handleTouchStartPan();
}
function handleTouchStartDollyRotate() {
if (scope.enableZoom) handleTouchStartDolly();
if (scope.enableRotate) handleTouchStartRotate();
}
function handleTouchMoveRotate(event) {
if (scope.pointers.length == 1) {
scope.rotateEnd.set(event.pageX, event.pageY);
} else {
const position = getSecondPointerPosition(event);
const x = 0.5 * (event.pageX + position.x);
const y = 0.5 * (event.pageY + position.y);
scope.rotateEnd.set(x, y);
}
scope.rotateDelta.subVectors(scope.rotateEnd, scope.rotateStart).multiplyScalar(scope.rotateSpeed);
const element = scope.domElement;
rotateLeft((2 * Math.PI * scope.rotateDelta.x) / element.clientHeight);
rotateUp((2 * Math.PI * scope.rotateDelta.y) / element.clientHeight);
scope.rotateStart.copy(scope.rotateEnd);
}
function handleTouchMovePan(event) {
if (scope.pointers.length === 1) {
scope.panEnd.set(event.pageX, event.pageY);
} else {
const position = getSecondPointerPosition(event);
const x = 0.5 * (event.pageX + position.x);
const y = 0.5 * (event.pageY + position.y);
scope.panEnd.set(x, y);
}
scope.panDelta.subVectors(scope.panEnd, scope.panStart).multiplyScalar(scope.panSpeed);
pan(scope.panDelta.x, scope.panDelta.y);
scope.panStart.copy(scope.panEnd);
}
function handleTouchMoveDolly(event) {
const position = getSecondPointerPosition(event);
const dx = event.pageX - position.x;
const dy = event.pageY - position.y;
const distance = Math.sqrt(dx * dx + dy * dy);
scope.dollyEnd.set(0, distance);
scope.dollyDelta.set(0, Math.pow(scope.dollyEnd.y / scope.dollyStart.y, scope.zoomSpeed));
dollyOut(scope.dollyDelta.y);
scope.dollyStart.copy(scope.dollyEnd);
}
function handleTouchMoveDollyPan(event) {
if (scope.enableZoom) handleTouchMoveDolly(event);
if (scope.enablePan) handleTouchMovePan(event);
}
function handleTouchMoveDollyRotate(event) {
if (scope.enableZoom) handleTouchMoveDolly(event);
if (scope.enableRotate) handleTouchMoveRotate(event);
}
function onPointerDown(event) {
if (scope.enabled === false) return;
if (scope.pointers.length === 0) {
scope.domElement.setPointerCapture(event.pointerId);
scope.domElement.addEventListener("pointermove", onPointerMove);
scope.domElement.addEventListener("pointerup", onPointerUp);
}
addPointer(event);
if (event.pointerType === "touch") {
onTouchStart(event);
} else {
onMouseDown(event);
}
}
function onPointerMove(event) {
if (scope.enabled === false) return;
if (event.pointerType === "touch") {
onTouchMove(event);
} else {
onMouseMove(event);
}
}
function onPointerUp(event) {
removePointer(event);
if (scope.pointers.length === 0) {
scope.domElement.releasePointerCapture(event.pointerId);
scope.domElement.removeEventListener("pointermove", onPointerMove);
scope.domElement.removeEventListener("pointerup", onPointerUp);
}
scope.dispatchEvent(_endEvent);
scope.state = STATE.NONE;
}
function onMouseDown(event) {
let mouseAction;
switch (event.button) {
case 0:
mouseAction = scope.mouseButtons.LEFT;
break;
case 1:
mouseAction = scope.mouseButtons.MIDDLE;
break;
case 2:
mouseAction = scope.mouseButtons.RIGHT;
break;
default:
mouseAction = -1;
}
switch (mouseAction) {
case MOUSE.DOLLY:
if (scope.enableZoom === false) return;
handleMouseDownDolly(event);
scope.state = STATE.DOLLY;
break;
case MOUSE.ROTATE:
if (event.ctrlKey || event.metaKey || event.shiftKey) {
if (scope.enablePan === false) return;
handleMouseDownPan(event);
scope.state = STATE.PAN;
} else {
if (scope.enableRotate === false) return;
handleMouseDownRotate(event);
scope.state = STATE.ROTATE;
}
break;
case MOUSE.PAN:
if (event.ctrlKey || event.metaKey || event.shiftKey) {
if (scope.enableRotate === false) return;
handleMouseDownRotate(event);
scope.state = STATE.ROTATE;
} else {
if (scope.enablePan === false) return;
handleMouseDownPan(event);
scope.state = STATE.PAN;
}
break;
default:
scope.state = STATE.NONE;
}
if (scope.state !== STATE.NONE) {
scope.dispatchEvent(_startEvent);
}
}
function onMouseMove(event) {
switch (scope.state) {
case STATE.ROTATE:
if (scope.enableRotate === false) return;
handleMouseMoveRotate(event);
break;
case STATE.DOLLY:
if (scope.enableZoom === false) return;
handleMouseMoveDolly(event);
break;
case STATE.PAN:
if (scope.enablePan === false) return;
handleMouseMovePan(event);
break;
}
}
function onMouseWheel(event) {
if (scope.enabled === false || scope.enableZoom === false || scope.state !== STATE.NONE) return;
event.preventDefault();
scope.dispatchEvent(_startEvent);
handleMouseWheel(event);
scope.dispatchEvent(_endEvent);
}
function onKeyDown(event) {
if (scope.enabled === false || scope.enablePan === false) return;
handleKeyDown(event);
}
function onTouchStart(event) {
trackPointer(event);
switch (scope.pointers.length) {
case 1:
switch (scope.touches.ONE) {
case TOUCH.ROTATE:
if (scope.enableRotate === false) return;
handleTouchStartRotate();
scope.state = STATE.TOUCH_ROTATE;
break;
case TOUCH.PAN:
if (scope.enablePan === false) return;
handleTouchStartPan();
scope.state = STATE.TOUCH_PAN;
break;
default:
scope.state = STATE.NONE;
}
break;
case 2:
switch (scope.touches.TWO) {
case TOUCH.DOLLY_PAN:
if (scope.enableZoom === false && scope.enablePan === false) return;
handleTouchStartDollyPan();
scope.state = STATE.TOUCH_DOLLY_PAN;
break;
case TOUCH.DOLLY_ROTATE:
if (scope.enableZoom === false && scope.enableRotate === false) return;
handleTouchStartDollyRotate();
scope.state = STATE.TOUCH_DOLLY_ROTATE;
break;
default:
scope.state = STATE.NONE;
}
break;
default:
scope.state = STATE.NONE;
}
if (scope.state !== STATE.NONE) {
scope.dispatchEvent(_startEvent);
}
}
function onTouchMove(event) {
trackPointer(event);
switch (scope.state) {
case STATE.TOUCH_ROTATE:
if (scope.enableRotate === false) return;
handleTouchMoveRotate(event);
scope.update();
break;
case STATE.TOUCH_PAN:
if (scope.enablePan === false) return;
handleTouchMovePan(event);
scope.update();
break;
case STATE.TOUCH_DOLLY_PAN:
if (scope.enableZoom === false && scope.enablePan === false) return;
handleTouchMoveDollyPan(event);
scope.update();
break;
case STATE.TOUCH_DOLLY_ROTATE:
if (scope.enableZoom === false && scope.enableRotate === false) return;
handleTouchMoveDollyRotate(event);
scope.update();
break;
default:
scope.state = STATE.NONE;
}
}
function onContextMenu(event) {
if (scope.enabled === false) return;
event.preventDefault();
}
function addPointer(event) {
scope.pointers.push(event);
}
function removePointer(event) {
delete scope.pointerPositions[event.pointerId];
for (let i = 0; i < scope.pointers.length; i++) {
if (scope.pointers[i].pointerId == event.pointerId) {
scope.pointers.splice(i, 1);
return;
}
}
}
function trackPointer(event) {
let position = scope.pointerPositions[event.pointerId];
if (position === undefined) {
position = new Vector2();
scope.pointerPositions[event.pointerId] = position;
}
position.set(event.pageX, event.pageY);
}
function getSecondPointerPosition(event) {
const pointer = event.pointerId === scope.pointers[0].pointerId ? scope.pointers[1] : scope.pointers[0];
return scope.pointerPositions[pointer.pointerId];
}
scope.domElement.addEventListener("contextmenu", onContextMenu);
scope.domElement.addEventListener("pointerdown", onPointerDown);
scope.domElement.addEventListener("pointercancel", onPointerUp);
scope.domElement.addEventListener("wheel", onMouseWheel, { passive: false });
this.update();
}
}
class OrbitDragger {
constructor(viewer) {
this.updateControls = () => {
this.orbit.target.copy(this.viewer.target);
this.orbit.update();
};
this.updateControlsCamera = () => {
this.orbit.maxDistance = this.viewer.camera.far;
this.orbit.minDistance = this.viewer.camera.near;
this.orbit.object = this.viewer.camera;
this.orbit.update();
};
this.optionsChange = ({ data: options }) => {
this.orbit.zoomSpeed = Math.abs(this.orbit.zoomSpeed) * (options.reverseZoomWheel ? -1 : 1);
};
this.controlsStart = () => {
this.changed = false;
};
this.controlsChange = () => {
this.viewer.target.copy(this.orbit.target);
this.viewer.update();
switch (this.orbit.state) {
case STATE.ROTATE:
case STATE.TOUCH_ROTATE:
this.viewer.emitEvent({
type: "orbit",
});
break;
case STATE.PAN:
case STATE.TOUCH_PAN:
this.viewer.emitEvent({
type: "pan",
x: this.orbit.panEnd.x,
y: this.orbit.panEnd.y,
dX: this.orbit.panDelta.x,
dY: this.orbit.panDelta.y,
});
break;
case STATE.DOLLY:
this.viewer.emitEvent({
type: "zoomat",
data: this.orbit.dollyScale,
x: this.orbit.dollyEnd.x,
y: this.orbit.dollyEnd.y,
});
break;
}
this.viewer.emitEvent({ type: "changecamera" });
this.changed = true;
};
this.stopContextMenu = (event) => {
if (this.changed) {
event.preventDefault();
event.stopPropagation();
}
};
this.orbit = new OrbitControls(viewer.camera, viewer.canvas);
this.orbit.mouseButtons = { LEFT: MOUSE.ROTATE, MIDDLE: MOUSE.PAN, RIGHT: MOUSE.PAN };
this.orbit.touches = { ONE: TOUCH.ROTATE, TWO: TOUCH.DOLLY_PAN };
this.orbit.screenSpacePanning = true;
this.orbit.rotateSpeed = 0.33;
this.orbit.addEventListener("start", this.controlsStart);
this.orbit.addEventListener("change", this.controlsChange);
this.changed = false;
this.viewer = viewer;
this.viewer.addEventListener("databasechunk", this.updateControls);
this.viewer.addEventListener("clear", this.updateControls);
this.viewer.on("viewposition", this.updateControls);
this.viewer.addEventListener("zoom", this.updateControls);
this.viewer.addEventListener("drawviewpoint", this.updateControls);
this.viewer.addEventListener("changecameramode", this.updateControlsCamera);
this.viewer.addEventListener("optionschange", this.optionsChange);
this.viewer.addEventListener("contextmenu", this.stopContextMenu);
this.updateControls();
}
initialize() { }
dispose() {
this.viewer.removeEventListener("databasechunk", this.updateControls);
this.viewer.removeEventListener("clear", this.updateControls);
this.viewer.off("viewposition", this.updateControls);
this.viewer.removeEventListener("zoom", this.updateControls);
this.viewer.removeEventListener("drawviewpoint", this.updateControls);
this.viewer.removeEventListener("changecameramode", this.updateControlsCamera);
this.viewer.removeEventListener("optionschange", this.optionsChange);
this.viewer.removeEventListener("contextmenu", this.stopContextMenu);
this.orbit.removeEventListener("start", this.controlsStart);
this.orbit.removeEventListener("change", this.controlsChange);
this.orbit.dispose();
}
}
class CuttingPlaneDragger extends OrbitDragger {
constructor(viewer, normal, color) {
super(viewer);
this.transformChange = () => {
this.plane.constant = -this.planeCenter.position.dot(this.plane.normal);
this.viewer.update();
};
this.transformDrag = (event) => {
this.orbit.enabled = !event.value;
};
this.updatePlaneSize = () => {
this.planeHelper.size = this.viewer.extents.getSize(new Vector3()).length();
this.viewer.update();
};
this.updateTransformCamera = () => {
this.transform.camera = this.viewer.camera;
};
this.onDoubleClick = (event) => {
event.stopPropagation();
this.plane.negate();
this.viewer.update();
};
const size = viewer.extents.getSize(new Vector3()).length();
const center = viewer.extents.getCenter(new Vector3());
const constant = -center.dot(normal);
this.plane = new Plane(normal, constant);
if (!viewer.renderer.clippingPlanes)
viewer.renderer.clippingPlanes = [];
viewer.renderer.clippingPlanes.push(this.plane);
this.planeHelper = new PlaneHelper(this.plane, size, color, center);
this.viewer.helpers.add(this.planeHelper);
this.planeCenter = new Object3D();
this.planeCenter.position.copy(viewer.extents.getCenter(new Vector3()));
this.viewer.helpers.add(this.planeCenter);
this.transform = new TransformControls(viewer.camera, viewer.canvas);
this.transform.showX = !!normal.x;
this.transform.showY = !!normal.y;
this.transform.showZ = !!normal.z;
this.transform.attach(this.planeCenter);
this.transform.addEventListener("change", this.transformChange);
this.transform.addEventListener("dragging-changed", this.transformDrag);
this.viewer.helpers.add(this.transform.getHelper());
this.viewer.addEventListener("explode", this.updatePlaneSize);
this.viewer.addEventListener("show", this.updatePlaneSize);
this.viewer.addEventListener("showall", this.updatePlaneSize);
this.viewer.addEventListener("changecameramode", this.updateTransformCamera);
this.viewer.canvas.addEventListener("dblclick", this.onDoubleClick, true);
this.viewer.update();
}
dispose() {
this.viewer.removeEventListener("explode", this.updatePlaneSize);
this.viewer.removeEventListener("show", this.updatePlaneSize);
this.viewer.removeEventListener("showall", this.updatePlaneSize);
this.viewer.removeEventListener("changecameramode", this.updateTransformCamera);
this.viewer.canvas.removeEventListener("dblclick", this.onDoubleClick, true);
this.transform.removeEventListener("change", this.transformChange);
this.transform.removeEventListener("dragging-changed", this.transformDrag);
this.transform.getHelper().removeFromParent();
this.transform.detach();
this.transform.dispose();
this.planeHelper.removeFromParent();
this.planeHelper.dispose();
this.planeCenter.removeFromParent();
super.dispose();
}
}
class CuttingPlaneXAxisDragger extends CuttingPlaneDragger {
constructor(viewer) {
super(viewer, new Vector3(1, 0, 0), 0xff0000);
}
}
class CuttingPlaneYAxisDragger extends CuttingPlaneDragger {
constructor(viewer) {
super(viewer, new Vector3(0, 1, 0), 0x00ff00);
}
}
class CuttingPlaneZAxisDragger extends CuttingPlaneDragger {
constructor(viewer) {
super(viewer, new Vector3(0, 0, 1), 0x0000ff);
}
}
const DisplayUnits = {
Meters: { name: "Meters", symbol: "m", scale: 1.0 },
Centimeters: { name: "Centimeters", symbol: "cm", scale: 0.01 },
Millimeters: { name: "Millimeters", symbol: "mm", scale: 0.001 },
Feet: { name: "Feet", symbol: "ft", scale: 0.3048 },
Inches: { name: "Inches", symbol: "in", scale: 0.0254 },
Yards: { name: "Yards", symbol: "yd", scale: 0.9144 },
Kilometers: { name: "Kilometers", symbol: "km", scale: 1000.0 },
Miles: { name: "Miles", symbol: "mi", scale: 1609.344 },
Micrometers: { name: "Micrometers", symbol: "µm", scale: 0.000001 },
Mils: { name: "Mils", symbol: "mil", scale: 0.0000254 },
MicroInches: { name: "Micro-inches", symbol: "µin", scale: 0.0000000254 },
Default: { name: "File units", symbol: "", scale: 1.0 },
};
const ModelUnits = {
Default: { name: "", symbol: "", scale: 1.0 },
};
Object.keys(DisplayUnits).forEach((key) => (ModelUnits[key] = DisplayUnits[key]));
Object.keys(DisplayUnits).forEach((key) => (ModelUnits[DisplayUnits[key].symbol] = DisplayUnits[key]));
function convertUnits(fromUnits, toUnits, distance) {
const fromFactor = 1 / (ModelUnits[fromUnits] || ModelUnits.Default).scale;
const toFactor = (ModelUnits[toUnits] || ModelUnits.Default).scale || 1;
return distance * fromFactor * toFactor;
}
function getDisplayUnit(units) {
return (ModelUnits[units] || ModelUnits.Default).symbol;
}
function calculatePrecision(value) {
const distance = Math.abs(value);
if (distance >= 1000)
return 0;
if (distance >= 10)
return 1;
if (distance >= 0.1)
return 2;
if (distance >= 0.001)
return 3;
return distance > 0 ? Math.floor(-Math.log10(distance)) + 1 : 2;
}
function formatNumber(distance, digits, precision) {
let result = distance.toFixed(digits);
if (precision === "Auto")
result = result.replace(/\.0+$/, "").replace(/\.$/, "");
if (+result !== distance)
result = "~ " + result;
return result;
}
function formatDistance(distance, units, precision = 2) {
let digits;
if (precision === "Auto")
digits = calculatePrecision(distance);
else if (Number.isFinite(precision))
digits = precision;
else
digits = parseFloat(precision);
if (!Number.isFinite(digits))
digits = 2;
else if (digits < 0)
digits = 0;
else if (digits > 10)
digits = 10;
if (ModelUnits[units]) {
return formatNumber(distance, digits, precision) + " " + ModelUnits[units].symbol;
}
else if (units) {
return formatNumber(distance, digits, precision) + " " + units;
}
else {
return formatNumber(distance, digits, precision);
}
}
const DESKTOP_SNAP_DISTANCE = 10;
const MOBILE_SNAP_DISTANCE = 50;
const _vertex = new Vector3();
const _start = new Vector3();
const _end = new Vector3();
const _line = new Line3();
const _center = new Vector3();
const _projection = new Vector3();
class Snapper {
constructor(camera, renderer, canvas) {
this.camera = camera;
this.renderer = renderer;
this.canvas = canvas;
this.threshold = 0.0001;
this.raycaster = new Raycaster();
this.detectRadiusInPixels = this.isMobile() ? MOBILE_SNAP_DISTANCE : DESKTOP_SNAP_DISTANCE;
this.edgesCache = new WeakMap();
}
isMobile() {
if (typeof navigator === "undefined")
return false;
return /Android|webOS|iPhone|iPad|iPod|BlackBerry|Opera Mini|Opera Mobi|IEMobile/i.test(navigator.userAgent);
}
getMousePosition(event, target) {
return target.set(event.clientX, event.clientY);
}
getPointerIntersects(mouse, objects) {
const rect = this.canvas.getBoundingClientRect();
const x = ((mouse.x - rect.left) / rect.width) * 2 - 1;
const y = (-(mouse.y - rect.top) / rect.height) * 2 + 1;
const coords = new Vector2(x, y);
this.raycaster.setFromCamera(coords, this.camera);
this.raycaster.params = {
Mesh: {},
Line: { threshold: this.threshold },
Line2: { threshold: this.threshold },
LOD: {},
Points: { threshold: this.threshold },
Sprite: {},
};
let intersects = this.raycaster.intersectObjects(objects, false);
(this.renderer.clippingPlanes || []).forEach((plane) => {
intersects = intersects.filter((intersect) => plane.distanceToPoint(intersect.point) >= 0);
});
return intersects;
}
getDetectRadius(point) {
const camera = this.camera;
if (camera.isOrthographicCamera) {
const fieldHeight = (camera.top - camera.bottom) / camera.zoom;
const canvasHeight = this.canvas.height;
const worldUnitsPerPixel = fieldHeight / canvasHeight;
return this.detectRadiusInPixels * worldUnitsPerPixel;
}
if (camera.isPerspectiveCamera) {
const distance = camera.position.distanceTo(point);
const fieldHeight = 2 * Math.tan(MathUtils.degToRad(camera.fov * 0.5)) * distance;
const canvasHeight = this.canvas.height;
const worldUnitsPerPixel = fieldHeight / canvasHeight;
return this.detectRadiusInPixels * worldUnitsPerPixel;
}
return 0.1;
}
getSnapPoint(mouse, objects) {
const intersections = this.getPointerIntersects(mouse, objects);
if (intersections.length === 0)
return undefined;
const object = intersections[0].object;
const intersectionPoint = intersections[0].point;
const localPoint = object.worldToLocal(intersectionPoint.clone());
let snapPoint;
let snapDistance = this.getDetectRadius(intersectionPoint);
const geometry = object.geometry;
const positions = geometry.attributes.position.array;
for (let i = 0; i < positions.length; i += 3) {
_vertex.set(positions[i], positions[i + 1], positions[i + 2]);
const distance = _vertex.distanceTo(localPoint);
if (distance < snapDistance) {
snapDistance = distance;
snapPoint = _vertex.clone();
}
}
if (snapPoint)
return object.localToWorld(snapPoint);
let edges = this.edgesCache.get(geometry);
if (!edges) {
edges = new EdgesGeometry(geometry);
this.edgesCache.set(geometry, edges);
}
const edgePositions = edges.attributes.position.array;
for (let i = 0; i < edgePositions.length; i += 6) {
_start.set(edgePositions[i], edgePositions[i + 1], edgePositions[i + 2]);
_end.set(edgePositions[i + 3], edgePositions[i + 4], edgePositions[i + 5]);
_line.set(_start, _end);
_line.getCenter(_center);
const centerDistance = _center.distanceTo(localPoint);
if (centerDistance < snapDistance) {
snapDistance = centerDistance;
snapPoint = _center.clone();
continue;
}
_line.closestPointToPoint(localPoint, true, _projection);
const lineDistance = _projection.distanceTo(localPoint);
if (lineDistance < snapDistance) {
snapDistance = lineDistance;
snapPoint = _projection.clone();
}
}
if (snapPoint)
return object.localToWorld(snapPoint);
return intersectionPoint.clone();
}
}
const _downPoint = new Vector2();
class MeasureLineDragger extends OrbitDragger {
constructor(viewer) {
super(viewer);
this.scale = 1.0;
this.units = "";
this.precision = 2;
this.onPointerDown = (event) => {
if (event.button !== 0)
return;
const mouse = this.snapper.getMousePosition(event, _downPoint);
this.line.startPoint = this.snapper.getSnapPoint(mouse, this.objects);
this.line.render();
this.viewer.canvas.setPointerCapture(event.pointerId);
this.orbit.enabled = !this.line.startPoint;
};
this.onPointerMove = (event) => {
if (this.orbit.enabled && this.orbit.state !== -1)
return;
const mouse = this.snapper.getMousePosition(event, _downPoint);
const snapPoint = this.snapper.getSnapPoint(mouse, this.objects);
if (snapPoint && this.line.endPoint && snapPoint.equals(this.line.endPoint))
return;
this.line.endPoint = snapPoint;
this.line.render();
if (this.line.startPoint)
this.changed = true;
};
this.onPointerUp = (event) => {
if (this.line.startPoint && this.line.endPoint && this.line.getDistance() > 0) {
this.line = new MeasureLine(this.overlay, this.scale, this.units, this.precision);
this.overlay.addLine(this.line);
}
else {
this.line.startPoint = undefined;
this.line.endPoint = undefined;
this.line.render();
}
this.viewer.canvas.releasePointerCapture(event.pointerId);
this.orbit.enabled = true;
};
this.onPointerCancel = (event) => {
this.viewer.canvas.dispatchEvent(new PointerEvent("pointerup", event));
};
this.onPointerLeave = () => {
this.line.endPoint = undefined;
this.line.render();
};
this.clearOverlay = () => {
this.overlay.clear();
this.line = new MeasureLine(this.overlay, this.scale, this.units, this.precision);
this.overlay.addLine(this.line);
};
this.renderOverlay = () => {
this.overlay.render();
};
this.updateObjects = () => {
this.objects.length = 0;
this.viewer.models.forEach((model) => {
model.getVisibleObjects().forEach((object) => this.objects.push(object));
});
};
this.updateSnapperCamera = () => {
this.snapper.camera = this.viewer.camera;
this.overlay.camera = this.viewer.camera;
};
this.updateUnits = () => {
var _a, _b;
const model = this.viewer.models[0];
const units = (_a = this.viewer.options.rulerUnit) !== null && _a !== void 0 ? _a : "Default";
const precision = (_b = this.viewer.options.rulerPrecision) !== null && _b !== void 0 ? _b : "Default";
if (units === "Default") {
this.scale = model.getUnitScale();
this.units = model.getUnitString();
}
else {
this.scale = convertUnits(model.getUnits(), units, 1);
this.units = units;
}
if (precision === "Default") {
this.precision = model.getPrecision();
}
else {
this.precision = precision;
}
this.overlay.updateLineUnits(this.scale, this.units, this.precision);
};
this.overlay = new MeasureOverlay(viewer.camera, viewer.canvas);
this.overlay.attach();
this.line = new MeasureLine(this.overlay, this.scale, this.units, this.precision);
this.overlay.addLine(this.line);
this.snapper = new Snapper(viewer.camera, viewer.renderer, viewer.canvas);
this.snapper.threshold = viewer.extents.getSize(new Vector3()).length() / 10000;
this.objects = [];
this.updateObjects();
this.updateUnits();
this.viewer.canvas.addEventListener("pointerdown", this.onPointerDown);
this.viewer.canvas.addEventListener("pointermove", this.onPointerMove);
this.viewer.canvas.addEventListener("pointerup", this.onPointerUp);
this.viewer.canvas.addEventListener("pointercancel", this.onPointerCancel);
this.viewer.canvas.addEventListener("pointerleave", this.onPointerLeave);
this.viewer.addEventListener("render", this.renderOverlay);
this.viewer.addEventListener("hide", this.updateObjects);
this.viewer.addEventListener("isolate", this.updateObjects);
this.viewer.addEventListener("show", this.updateObjects);
this.viewer.addEventListener("showall", this.updateObjects);
this.viewer.addEventListener("changecameramode", this.updateSnapperCamera);
this.viewer.addEventListener("optionschange", this.updateUnits);
}
dispose() {
this.viewer.canvas.removeEventListener("pointerdown", t