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