UNPKG

@inweb/viewer-three

Version:

JavaScript library for rendering CAD and BIM files in a browser using Three.js

1,196 lines (1,139 loc) 221 kB
import { draggersRegistry, commandsRegistry, componentsRegistry, Loader, loadersRegistry, Options, 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, Matrix4, Vector4, Raycaster, Controls, Clock, Sphere, MathUtils, Box3, Color, AmbientLight, DirectionalLight, HemisphereLight, MeshPhongMaterial, WebGLRenderTarget, UnsignedByteType, RGBAFormat, EdgesGeometry, OrthographicCamera, CylinderGeometry, Sprite, CanvasTexture, SRGBColorSpace, SpriteMaterial, LoadingManager, LoaderUtils, TextureLoader, BufferAttribute, PointsMaterial, Points, TriangleStripDrawMode, TriangleFanDrawMode, LineSegments, LineLoop, Group, NormalBlending, PerspectiveCamera, 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 { GLTFLoader } from "three/examples/jsm/loaders/GLTFLoader.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 = 16776960, 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: 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: color, opacity: .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(.5 * this.size, .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 = .05; this.enableZoom = true; this.zoomSpeed = 1; this.enableRotate = true; this.rotateSpeed = 1; this.enablePan = true; this.panSpeed = 1; this.screenSpacePanning = true; this.keyPanSpeed = 7; this.autoRotate = false; this.autoRotateSpeed = 2; 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 = 1e-6; 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(.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); 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 = .5 * (scope.pointers[0].pageX + scope.pointers[1].pageX); const y = .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 = .5 * (scope.pointers[0].pageX + scope.pointers[1].pageX); const y = .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 = .5 * (event.pageX + position.x); const y = .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 = .5 * (event.pageX + position.x); const y = .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.maxDistance = this.viewer.camera.far; this.orbit.minDistance = this.viewer.camera.near; this.orbit.object = this.viewer.camera; this.orbit.target.copy(this.viewer.target); this.orbit.update(); }; this.controlsStart = () => { this.changed = false; }; this.controlsChange = () => { this.viewer.target.copy(this.orbit.target); this.viewer.update(); switch (this.orbit.state) { 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.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 = .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.on("viewposition", this.updateControls); this.viewer.addEventListener("zoom", this.updateControls); this.viewer.addEventListener("drawviewpoint", this.updateControls); this.viewer.addEventListener("contextmenu", this.stopContextMenu); this.updateControls(); } initialize() {} dispose() { this.viewer.removeEventListener("databasechunk", this.updateControls); this.viewer.off("viewposition", this.updateControls); this.viewer.removeEventListener("zoom", this.updateControls); this.viewer.removeEventListener("drawviewpoint", this.updateControls); 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.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.on("explode", this.updatePlaneSize); this.viewer.on("show", this.updatePlaneSize); this.viewer.on("showall", this.updatePlaneSize); this.viewer.canvas.addEventListener("dblclick", this.onDoubleClick, true); this.viewer.update(); } dispose() { this.viewer.off("explode", this.updatePlaneSize); this.viewer.off("show", this.updatePlaneSize); this.viewer.off("showAll", this.updatePlaneSize); 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), 16711680); } } class CuttingPlaneYAxisDragger extends CuttingPlaneDragger { constructor(viewer) { super(viewer, new Vector3(0, 1, 0), 65280); } } class CuttingPlaneZAxisDragger extends CuttingPlaneDragger { constructor(viewer) { super(viewer, new Vector3(0, 0, 1), 255); } } const PRECISION = .01; class MeasureLineDragger extends OrbitDragger { constructor(viewer) { super(viewer); this.onPointerDown = event => { if (event.button !== 0) return; this.line.startPoint = this.snapper.getSnapPoint(event); 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; this.line.endPoint = this.snapper.getSnapPoint(event); this.line.render(); if (this.line.startPoint) this.changed = true; }; this.onPointerUp = event => { if (this.line.startPoint && this.line.endPoint && this.line.getDistance() >= PRECISION) { this.line = new MeasureLine(this.overlay); 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.renderOverlay = () => { this.overlay.render(); }; this.updateSnapper = () => { this.snapper.update(this.viewer.scene); }; this.overlay = new MeasureOverlay(viewer.camera, viewer.canvas); this.overlay.attach(); this.line = new MeasureLine(this.overlay); this.overlay.addLine(this.line); this.snapper = new MeasureSnapper(viewer.camera, viewer.canvas); this.snapper.update(viewer.scene); 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.updateSnapper); this.viewer.addEventListener("isolate", this.updateSnapper); this.viewer.addEventListener("show", this.updateSnapper); this.viewer.addEventListener("showall", this.updateSnapper); } dispose() { this.viewer.canvas.removeEventListener("pointerdown", this.onPointerDown); this.viewer.canvas.removeEventListener("pointermove", this.onPointerMove); this.viewer.canvas.removeEventListener("pointerup", this.onPointerUp); this.viewer.canvas.removeEventListener("pointercancel", this.onPointerCancel); this.viewer.canvas.removeEventListener("pointerleave", this.onPointerLeave); this.viewer.removeEventListener("render", this.renderOverlay); this.viewer.removeEventListener("hide", this.updateSnapper); this.viewer.removeEventListener("isolate", this.updateSnapper); this.viewer.removeEventListener("show", this.updateSnapper); this.viewer.removeEventListener("showall", this.updateSnapper); this.overlay.detach(); this.overlay.dispose(); super.dispose(); } } class MeasureSnapper { constructor(camera, canvas) { this.objects = []; this.camera = camera; this.canvas = canvas; this.raycaster = new Raycaster; } getSnapPoint(event) { const mouse = new Vector2(event.clientX, event.clientY); 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: .25 }, Line2: { threshold: .25 }, LOD: {}, Points: { threshold: .1 }, Sprite: {} }; const intersects = this.raycaster.intersectObjects(this.objects, false); if (intersects.length === 0) return undefined; return intersects[0].point; } update(scene) { this.objects = []; scene.traverseVisible((child => this.objects.push(child))); } } class MeasureOverlay { constructor(camera, canvas) { this.lines = []; this.camera = camera; this.canvas = canvas; this.projector = new MeasureProjector(camera, canvas); } attach() { this.container = document.createElement("div"); this.container.id = "measure-container"; this.container.style.background = "rgba(0,0,0,0)"; this.container.style.position = "absolute"; this.container.style.top = "0px"; this.container.style.left = "0px"; this.container.style.width = "100%"; this.container.style.height = "100%"; this.container.style.outline = "none"; this.container.style.pointerEvents = "none"; this.container.style.overflow = "hidden"; this.canvas.parentElement.appendChild(this.container); } dispose() { this.clear(); } detach() { this.container.remove(); this.container = undefined; } clear() { this.lines.forEach((line => line.dispose())); this.lines = []; } render() { this.projector.updateProjectionMatrix(); this.lines.forEach((line => line.render())); } update() { this.lines.forEach((line => line.update())); } addLine(line) { this.lines.push(line); } removeLine(line) { this.lines = this.lines.filter((x => x !== line)); } } const _middlePoint = new Vector3; class MeasureLine { constructor(overlay) { this.id = Date.now(); this.unit = ""; this.scale = 1; this.size = 10; this.lineWidth = 2; this.style = { border: "2px solid #FFFFFF", background: "#009bff", boxShadow: "0 0 10px rgba(0,0,0,0.5)", color: "white", font: "1rem system-ui" }; this.overlay = overlay; this.elementStartPoint = overlay.container.appendChild(document.createElement("div")); this.elementEndPoint = overlay.container.appendChild(document.createElement("div")); this.elementLine = overlay.container.appendChild(document.createElement("div")); this.elementLabel = overlay.container.appendChild(document.createElement("div")); this.update(); } dispose() { this.elementStartPoint.remove(); this.elementEndPoint.remove(); this.elementLine.remove(); this.elementLabel.remove(); } render() { const projector = this.overlay.projector; if (this.startPoint) { const {point: point, visible: visible} = projector.projectPoint(this.startPoint); this.elementStartPoint.style.display = visible ? "block" : "none"; this.elementStartPoint.style.left = `${point.x}px`; this.elementStartPoint.style.top = `${point.y}px`; } else { this.elementStartPoint.style.display = "none"; } if (this.endPoint) { const {point: point, visible: visible} = projector.projectPoint(this.endPoint); this.elementEndPoint.style.display = visible ? "block" : "none"; this.elementEndPoint.style.left = `${point.x}px`; this.elementEndPoint.style.top = `${point.y}px`; } else { this.elementEndPoint.style.display = "none"; } if (this.startPoint && this.endPoint) { const {point1: point1, point2: point2, visible: visible} = projector.projectLine(this.startPoint, this.endPoint); point2.sub(point1); const angle = point2.angle(); const width = point2.length(); this.elementLine.style.display = visible ? "block" : "none"; this.elementLine.style.left = `${point1.x}px`; this.elementLine.style.top = `${point1.y}px`; this.elementLine.style.width = `${width}px`; this.elementLine.style.transform = `translate(0px, ${-this.lineWidth / 2}px) rotate(${angle}rad)`; } else { this.elementLine.style.display = "none"; } if (this.startPoint && this.endPoint) { _middlePoint.lerpVectors(this.startPoint, this.endPoint, .5); const {point: point, visible: visible} = projector.projectPoint(_middlePoint); const distance = this.getDistance(); this.elementLabel.style.display = visible && distance >= PRECISION ? "block" : "none"; this.elementLabel.style.left = `${point.x}px`; this.elementLabel.style.top = `${point.y}px`; this.elementLabel.innerHTML = `${distance.toFixed(2)} ${this.unit}`; } else { this.elementLabel.style.display = "none"; } } update() { this.elementStartPoint.id = `markup-dot-start-${this.id}`; this.elementStartPoint.style.position = "absolute"; this.elementStartPoint.style.zIndex = "2"; this.elementStartPoint.style.width = `${this.size}px`; this.elementStartPoint.style.height = `${this.size}px`; this.elementStartPoint.style.border = this.style.border; this.elementStartPoint.style.borderRadius = `${this.size}px`; this.elementStartPoint.style.background = this.style.background; this.elementStartPoint.style.boxShadow = this.style.boxShadow; this.elementStartPoint.style.transform = "translate(-50%, -50%)"; this.elementEndPoint.id = `markup-dot-end-${this.id}`; this.elementEndPoint.style.position = "absolute"; this.elementEndPoint.style.zIndex = "2"; this.elementEndPoint.style.width = `${this.size}px`; this.elementEndPoint.style.height = `${this.size}px`; this.elementEndPoint.style.border = this.style.border; this.elementEndPoint.style.borderRadius = `${this.size}px`; this.elementEndPoint.style.background = this.style.background; this.elementEndPoint.style.boxShadow = this.style.boxShadow; this.elementEndPoint.style.transform = "translate(-50%, -50%)"; this.elementLine.id = `markup-line-${this.id}`; this.elementLine.style.position = "absolute"; this.elementLine.style.zIndex = "1"; this.elementLine.style.height = `${this.lineWidth}px`; this.elementLine.style.background = this.style.background; this.elementLine.style.boxShadow = this.style.boxShadow; this.elementLine.style.transformOrigin = `0px ${this.lineWidth / 2}px`; this.elementLabel.id = `markup-label-${this.id}`; this.elementLabel.style.position = "absolute"; this.elementLabel.style.zIndex = "3"; this.elementLa