@polygonjs/polygonjs
Version:
node-based WebGL 3D engine https://polygonjs.com
496 lines (495 loc) • 16.8 kB
JavaScript
"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 });
}
}