UNPKG

@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
/////////////////////////////////////////////////////////////////////////////// // 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