UNPKG

@polygonjs/polygonjs

Version:

node-based WebGL 3D engine https://polygonjs.com

496 lines (495 loc) 16.8 kB
"use strict"; import { EventDispatcher, Vector2, Vector3, Box3, TOUCH, MOUSE } from "three"; 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, }; const EPS = 1e-6; export class PanZoomControls extends EventDispatcher { constructor(object, domElement) { super(); this.object = object; this.domElement = domElement; this.enabled = true; this.enablePan = true; this.enableZoom = true; this.enableDamping = true; this.dampingFactor = 0.05; this.panSpeed = 1; this.minZoom = 0.1; this.maxZoom = 50; this.zoomSpeed = 1; this.screenSpacePanning = true; this.zoomToCursor = true; // threshold this.panThreshold = 0; this.dollyThreshold = 0; this._panMoveDistanceTotal = 0; this._dollyMoveDistanceTotal = 0; // this.clampPosition = false; this.positionBounds = new Box3( new Vector3(-Infinity, -Infinity, -Infinity), new Vector3(Infinity, Infinity, Infinity) ); this.mouseButtons = { LEFT: MOUSE.ROTATE, MIDDLE: MOUSE.DOLLY, RIGHT: MOUSE.PAN }; this.touches = { ONE: TOUCH.PAN, TWO: TOUCH.DOLLY_PAN }; const panStart = new Vector2(); const panEnd = new Vector2(); const panDelta = new Vector2(); const panOffset = new Vector3(); const dollyStart = new Vector2(); const dollyEnd = new Vector2(); const dollyDelta = new Vector2(); this.domElement.style.touchAction = "none"; const pointers = []; const pointerPositions = {}; let state = STATE.NONE; let scale = 1; const dollyDirection = new Vector3(); const mouse = new Vector2(); let performCursorZoom = false; const mouseBefore = new Vector3(0, 0, 0); const mouseAfter = new Vector3(0, 0, 0); const getZoomScale = () => { return Math.pow(0.95, this.zoomSpeed); }; function addPointer(event) { pointers.push(event); } function removePointer(event) { delete pointerPositions[event.pointerId]; for (let i = 0; i < pointers.length; i++) { if (pointers[i].pointerId == event.pointerId) { pointers.splice(i, 1); return; } } } function trackPointer(event) { let position = pointerPositions[event.pointerId]; if (position === void 0) { position = new Vector2(); pointerPositions[event.pointerId] = position; } position.set(event.pageX, event.pageY); } const onContextMenu = (event) => { if (this.enabled === false) return; event.preventDefault(); }; const onPointerDown = (event) => { if (this.enabled === false) return; if (pointers.length === 0) { this.domElement.setPointerCapture(event.pointerId); this.domElement.ownerDocument.addEventListener("pointermove", onPointerMove); this.domElement.ownerDocument.addEventListener("pointerup", onPointerUp); } addPointer(event); if (event.pointerType === "touch") { onTouchStart(event); } else { onMouseDown(event); } }; const onPointerMove = (event) => { if (this.enabled === false) return; if (event.pointerType === "touch") { onTouchMove(event); } else { onMouseMove(event); } }; const onPointerUp = (event) => { removePointer(event); if (pointers.length === 0) { this.domElement.releasePointerCapture(event.pointerId); this.domElement.ownerDocument.removeEventListener("pointermove", onPointerMove); this.domElement.ownerDocument.removeEventListener("pointerup", onPointerUp); } this.dispatchEvent(_endEvent); state = STATE.NONE; }; const getSecondPointerPosition = (event) => { const pointer = event.pointerId === pointers[0].pointerId ? pointers[1] : pointers[0]; return pointerPositions[pointer.pointerId]; }; const handleTouchStartPan = () => { if (pointers.length === 1) { panStart.set(pointers[0].pageX, pointers[0].pageY); } else { const x = 0.5 * (pointers[0].pageX + pointers[1].pageX); const y = 0.5 * (pointers[0].pageY + pointers[1].pageY); panStart.set(x, y); } this._panMoveDistanceTotal = 0; }; const handleTouchStartDolly = () => { const dx = pointers[0].pageX - pointers[1].pageX; const dy = pointers[0].pageY - pointers[1].pageY; const distance = Math.sqrt(dx * dx + dy * dy); dollyStart.set(0, distance); this._dollyMoveDistanceTotal = 0; }; const handleTouchStartDollyPan = () => { if (this.enableZoom) handleTouchStartDolly(); if (this.enablePan) handleTouchStartPan(); }; const handleMouseDownDolly = (event) => { updateMouseParameters(event); dollyStart.set(event.clientX, event.clientY); this._dollyMoveDistanceTotal = 0; }; const handleMouseDownPan = (event) => { panStart.set(event.clientX, event.clientY); this._panMoveDistanceTotal = 0; }; const handleTouchMovePan = (event) => { if (pointers.length === 1) { 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); panEnd.set(x, y); } panDelta.subVectors(panEnd, panStart).multiplyScalar(this.panSpeed); this._panMoveDistanceTotal += panDelta.length(); if (this._panMoveDistanceTotal > this.panThreshold) { pan(panDelta.x, panDelta.y); } panStart.copy(panEnd); }; const 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); dollyEnd.set(0, distance); dollyDelta.set(0, Math.pow(dollyEnd.y / dollyStart.y, this.zoomSpeed)); this._dollyMoveDistanceTotal += dollyDelta.length(); if (this._dollyMoveDistanceTotal > this.dollyThreshold) { dollyOut(dollyDelta.y); } dollyStart.copy(dollyEnd); }; const handleTouchMoveDollyPan = (event) => { if (this.enableZoom) handleTouchMoveDolly(event); if (this.enablePan) handleTouchMovePan(event); }; const handleMouseMoveDolly = (event) => { dollyEnd.set(event.clientX, event.clientY); dollyDelta.subVectors(dollyEnd, dollyStart); this._dollyMoveDistanceTotal += dollyDelta.length(); if (this._dollyMoveDistanceTotal > this.dollyThreshold) { if (dollyDelta.y > 0) { dollyOut(getZoomScale()); } else if (dollyDelta.y < 0) { dollyIn(getZoomScale()); } } dollyStart.copy(dollyEnd); this.update(null); }; const handleMouseMovePan = (event) => { panEnd.set(event.clientX, event.clientY); panDelta.subVectors(panEnd, panStart).multiplyScalar(this.panSpeed); this._panMoveDistanceTotal += panDelta.length(); if (this._panMoveDistanceTotal > this.panThreshold) { pan(panDelta.x, panDelta.y); } panStart.copy(panEnd); this.update(null); }; const dollyOut = (dollyScale) => { if (this.object.isPerspectiveCamera || this.object.isOrthographicCamera) { scale /= dollyScale; } else { console.warn("WARNING: OrbitControls.js encountered an unknown camera type - dolly/zoom disabled."); this.enableZoom = false; } }; const dollyIn = (dollyScale) => { if (this.object.isPerspectiveCamera || this.object.isOrthographicCamera) { scale *= dollyScale; } else { console.warn("WARNING: OrbitControls.js encountered an unknown camera type - dolly/zoom disabled."); this.enableZoom = false; } }; const handleMouseWheel = (event) => { updateMouseParameters(event); if (event.deltaY < 0) { dollyIn(getZoomScale()); } else if (event.deltaY > 0) { dollyOut(getZoomScale()); } this.update(null); }; const onMouseWheel = (event) => { if (this.enabled === false || this.enableZoom === false || state !== STATE.NONE) return; event.preventDefault(); this.dispatchEvent(_startEvent); handleMouseWheel(event); this.dispatchEvent(_endEvent); }; const panLeft = (() => { const v = new Vector3(); const _panLeft = (distance, objectMatrix) => { v.setFromMatrixColumn(objectMatrix, 0); v.multiplyScalar(-distance); panOffset.add(v); }; return _panLeft; })(); const panUp = (() => { const v = new Vector3(); const _panUp = (distance, objectMatrix) => { if (this.screenSpacePanning === true) { v.setFromMatrixColumn(objectMatrix, 1); } else { v.setFromMatrixColumn(objectMatrix, 0); v.crossVectors(this.object.up, v); } v.multiplyScalar(distance); panOffset.add(v); }; return _panUp; })(); const pan = (() => { const _pan = (deltaX, deltaY) => { const element = this.domElement; if (this.object.isPerspectiveCamera) { const targetDistance = 1; panLeft(2 * deltaX * targetDistance / element.clientHeight, this.object.matrix); panUp(2 * deltaY * targetDistance / element.clientHeight, this.object.matrix); } else if (this.object.isOrthographicCamera) { panLeft( deltaX * (this.object.right - this.object.left) / this.object.zoom / element.clientWidth, this.object.matrix ); panUp( deltaY * (this.object.top - this.object.bottom) / this.object.zoom / element.clientHeight, this.object.matrix ); } else { console.warn("WARNING: PanZoomControls.js encountered an unknown camera type - pan disabled."); this.enablePan = false; } }; return _pan; })(); const updateMouseParameters = (event) => { if (!this.zoomToCursor) { return; } performCursorZoom = true; const rect = this.domElement.getBoundingClientRect(); const x = event.clientX - rect.left; const y = event.clientY - rect.top; const w = rect.width; const h = rect.height; mouse.x = x / w * 2 - 1; mouse.y = -(y / h) * 2 + 1; dollyDirection.set(mouse.x, mouse.y, 1).unproject(this.object).sub(this.object.position).normalize(); }; const onTouchStart = (event) => { trackPointer(event); switch (pointers.length) { case 1: switch (this.touches.ONE) { case TOUCH.PAN: if (this.enablePan === false) return; handleTouchStartPan(); state = STATE.TOUCH_PAN; break; default: state = STATE.NONE; } break; case 2: switch (this.touches.TWO) { case TOUCH.DOLLY_PAN: if (this.enableZoom === false && this.enablePan === false) return; handleTouchStartDollyPan(); state = STATE.TOUCH_DOLLY_PAN; break; default: state = STATE.NONE; } break; default: state = STATE.NONE; } if (state !== STATE.NONE) { this.dispatchEvent(_startEvent); } }; const onMouseDown = (event) => { let mouseAction; switch (event.button) { case 0: mouseAction = this.mouseButtons.LEFT; break; case 1: mouseAction = this.mouseButtons.MIDDLE; break; case 2: mouseAction = this.mouseButtons.RIGHT; break; default: mouseAction = -1; } switch (mouseAction) { case MOUSE.DOLLY: if (this.enableZoom === false) return; handleMouseDownDolly(event); state = STATE.DOLLY; break; case MOUSE.PAN: if (this.enablePan === false) return; handleMouseDownPan(event); state = STATE.PAN; break; default: state = STATE.NONE; } if (state !== STATE.NONE) { this.dispatchEvent(_startEvent); } }; const onTouchMove = (event) => { trackPointer(event); switch (state) { case STATE.TOUCH_PAN: if (this.enablePan === false) return; handleTouchMovePan(event); this.update(null); break; case STATE.TOUCH_DOLLY_PAN: if (this.enableZoom === false && this.enablePan === false) return; handleTouchMoveDollyPan(event); this.update(null); break; default: state = STATE.NONE; } }; const onMouseMove = (event) => { switch (state) { case STATE.DOLLY: if (this.enableZoom === false) return; handleMouseMoveDolly(event); break; case STATE.PAN: if (this.enablePan === false) return; handleMouseMovePan(event); break; } }; this.update = (() => { const lastPosition = new Vector3(); const _update = (deltaTime = null) => { const position = this.object.position; if (this.enableDamping === true) { this.object.position.addScaledVector(panOffset, this.dampingFactor); } else { this.object.position.add(panOffset); } const _clampPosition = () => { if (this.clampPosition) { position.clamp(this.positionBounds.min, this.positionBounds.max); } }; _clampPosition(); if (this.enableDamping === true) { panOffset.multiplyScalar(1 - this.dampingFactor); } else { panOffset.set(0, 0, 0); } let zoomChanged = false; if (this.zoomToCursor && performCursorZoom) { if (this.object.isPerspectiveCamera) { console.warn("zoom currently unsupported for perspective cameras"); } else if (this.object.isOrthographicCamera) { mouseBefore.set(mouse.x, mouse.y, 0); mouseAfter.set(mouse.x, mouse.y, 0); mouseBefore.unproject(this.object); this.object.zoom = Math.max( this.minZoom, Math.min(this.maxZoom, this.object.zoom / scale) ); this.object.updateProjectionMatrix(); zoomChanged = true; mouseAfter.unproject(this.object); this.object.position.sub(mouseAfter).add(mouseBefore); _clampPosition(); this.object.updateMatrixWorld(); } else { console.warn( "WARNING: OrbitControls.js encountered an unknown camera type - zoom to cursor disabled." ); this.zoomToCursor = false; } } if (!zoomChanged) { if (this.object.isOrthographicCamera) { this.object.zoom = Math.max( this.minZoom, Math.min(this.maxZoom, this.object.zoom / scale) ); this.object.updateProjectionMatrix(); zoomChanged = true; } } scale = 1; performCursorZoom = false; if (zoomChanged || lastPosition.distanceToSquared(this.object.position) > EPS) { this.dispatchEvent(_changeEvent); lastPosition.copy(this.object.position); zoomChanged = false; return true; } return false; }; return _update; })(); this.dispose = () => { this.domElement.removeEventListener("contextmenu", onContextMenu); this.domElement.removeEventListener("pointerdown", onPointerDown); this.domElement.removeEventListener("pointercancel", onPointerUp); this.domElement.removeEventListener("wheel", onMouseWheel); this.domElement.ownerDocument.removeEventListener("pointermove", onPointerMove); this.domElement.ownerDocument.removeEventListener("pointerup", onPointerUp); }; this.domElement.addEventListener("contextmenu", onContextMenu); this.domElement.addEventListener("pointerdown", onPointerDown); this.domElement.addEventListener("pointercancel", onPointerUp); this.domElement.addEventListener("wheel", onMouseWheel, { passive: false }); } }