gsplat-editor / editor /src /OrbitControls.ts
dylanebert's picture
dylanebert HF staff
fix download bug
18a4acc
import * as SPLAT from "gsplat";
class OrbitControls {
public enabled: boolean = true;
minAngle: number = -90;
maxAngle: number = 90;
minZoom: number = 0.1;
maxZoom: number = 50;
orbitSpeed: number = 1.75;
panSpeed: number = 1.25;
zoomSpeed: number = 2;
dampening: number = 0.5;
setCameraTarget: (newTarget: SPLAT.Vector3) => void = () => {};
update: () => void;
dispose: () => void;
constructor(
camera: SPLAT.Camera,
canvas: HTMLElement,
alpha: number = 0.5,
beta: number = 0.5,
radius: number = 13,
inputTarget: SPLAT.Vector3 = new SPLAT.Vector3(),
) {
let target = inputTarget.clone();
let desiredTarget = target.clone();
let desiredAlpha = alpha;
let desiredBeta = beta;
let desiredRadius = radius;
let dragging = false;
let panning = false;
let lastDist = 0;
let lastX = 0;
let lastY = 0;
let isUpdatingCamera = false;
const onCameraChange = () => {
if (isUpdatingCamera) return;
const eulerRotation = camera.rotation.toEuler();
desiredAlpha = -eulerRotation.y;
desiredBeta = -eulerRotation.x;
const x = camera.position.x - desiredRadius * Math.sin(desiredAlpha) * Math.cos(desiredBeta);
const y = camera.position.y + desiredRadius * Math.sin(desiredBeta);
const z = camera.position.z + desiredRadius * Math.cos(desiredAlpha) * Math.cos(desiredBeta);
desiredTarget = new SPLAT.Vector3(x, y, z);
};
camera.addEventListener("objectChanged", onCameraChange);
this.setCameraTarget = (newTarget: SPLAT.Vector3) => {
const dx = newTarget.x - camera.position.x;
const dy = newTarget.y - camera.position.y;
const dz = newTarget.z - camera.position.z;
desiredRadius = Math.sqrt(dx * dx + dy * dy + dz * dz);
desiredBeta = Math.atan2(dy, Math.sqrt(dx * dx + dz * dz));
desiredAlpha = -Math.atan2(dx, dz);
desiredTarget = new SPLAT.Vector3(newTarget.x, newTarget.y, newTarget.z);
};
const computeZoomNorm = () => {
return 0.1 + (0.9 * (desiredRadius - this.minZoom)) / (this.maxZoom - this.minZoom);
};
const onMouseDown = (e: MouseEvent) => {
preventDefault(e);
if (!this.enabled) return;
if (e.button === 1) {
dragging = true;
panning = e.shiftKey;
lastX = e.clientX;
lastY = e.clientY;
} else if (e.altKey && e.button === 0) {
dragging = true;
panning = false;
lastX = e.clientX;
lastY = e.clientY;
} else if (e.altKey && e.button === 2) {
dragging = true;
panning = true;
lastX = e.clientX;
lastY = e.clientY;
}
};
const onMouseUp = (e: MouseEvent) => {
preventDefault(e);
dragging = false;
panning = false;
};
const onMouseMove = (e: MouseEvent) => {
preventDefault(e);
if (!this.enabled || !dragging || !camera) return;
const dx = e.clientX - lastX;
const dy = e.clientY - lastY;
if (panning) {
const zoomNorm = computeZoomNorm();
const panX = -dx * this.panSpeed * 0.01 * zoomNorm;
const panY = -dy * this.panSpeed * 0.01 * zoomNorm;
const R = SPLAT.Matrix3.RotationFromQuaternion(camera.rotation).buffer;
const right = new SPLAT.Vector3(R[0], R[3], R[6]);
const up = new SPLAT.Vector3(R[1], R[4], R[7]);
desiredTarget = desiredTarget.add(right.multiply(panX));
desiredTarget = desiredTarget.add(up.multiply(panY));
} else {
desiredAlpha -= dx * this.orbitSpeed * 0.003;
desiredBeta += dy * this.orbitSpeed * 0.003;
desiredBeta = Math.min(
Math.max(desiredBeta, (this.minAngle * Math.PI) / 180),
(this.maxAngle * Math.PI) / 180,
);
}
lastX = e.clientX;
lastY = e.clientY;
};
const onWheel = (e: WheelEvent) => {
preventDefault(e);
if (!this.enabled) return;
const zoomNorm = computeZoomNorm();
desiredRadius += e.deltaY * this.zoomSpeed * 0.025 * zoomNorm;
desiredRadius = Math.min(Math.max(desiredRadius, this.minZoom), this.maxZoom);
};
const onTouchStart = (e: TouchEvent) => {
preventDefault(e);
if (!this.enabled) return;
if (e.touches.length === 1) {
dragging = true;
panning = false;
lastX = e.touches[0].clientX;
lastY = e.touches[0].clientY;
lastDist = 0;
} else if (e.touches.length === 2) {
dragging = true;
panning = true;
lastX = (e.touches[0].clientX + e.touches[1].clientX) / 2;
lastY = (e.touches[0].clientY + e.touches[1].clientY) / 2;
const distX = e.touches[0].clientX - e.touches[1].clientX;
const distY = e.touches[0].clientY - e.touches[1].clientY;
lastDist = Math.sqrt(distX * distX + distY * distY);
}
};
const onTouchEnd = (e: TouchEvent) => {
preventDefault(e);
dragging = false;
panning = false;
};
const onTouchMove = (e: TouchEvent) => {
preventDefault(e);
if (!this.enabled || !dragging || !camera) return;
if (panning) {
const zoomNorm = computeZoomNorm();
const distX = e.touches[0].clientX - e.touches[1].clientX;
const distY = e.touches[0].clientY - e.touches[1].clientY;
const dist = Math.sqrt(distX * distX + distY * distY);
const delta = lastDist - dist;
desiredRadius += delta * this.zoomSpeed * 0.1 * zoomNorm;
desiredRadius = Math.min(Math.max(desiredRadius, this.minZoom), this.maxZoom);
lastDist = dist;
const touchX = (e.touches[0].clientX + e.touches[1].clientX) / 2;
const touchY = (e.touches[0].clientY + e.touches[1].clientY) / 2;
const dx = touchX - lastX;
const dy = touchY - lastY;
const R = SPLAT.Matrix3.RotationFromQuaternion(camera.rotation).buffer;
const right = new SPLAT.Vector3(R[0], R[3], R[6]);
const up = new SPLAT.Vector3(R[1], R[4], R[7]);
desiredTarget = desiredTarget.add(right.multiply(-dx * this.panSpeed * 0.025 * zoomNorm));
desiredTarget = desiredTarget.add(up.multiply(-dy * this.panSpeed * 0.025 * zoomNorm));
lastX = touchX;
lastY = touchY;
} else {
const dx = e.touches[0].clientX - lastX;
const dy = e.touches[0].clientY - lastY;
desiredAlpha -= dx * this.orbitSpeed * 0.003;
desiredBeta += dy * this.orbitSpeed * 0.003;
desiredBeta = Math.min(
Math.max(desiredBeta, (this.minAngle * Math.PI) / 180),
(this.maxAngle * Math.PI) / 180,
);
lastX = e.touches[0].clientX;
lastY = e.touches[0].clientY;
}
};
const lerp = (a: number, b: number, t: number) => {
return (1 - t) * a + t * b;
};
this.update = () => {
isUpdatingCamera = true;
alpha = lerp(alpha, desiredAlpha, this.dampening);
beta = lerp(beta, desiredBeta, this.dampening);
radius = lerp(radius, desiredRadius, this.dampening);
target = target.lerp(desiredTarget, this.dampening);
const x = target.x + radius * Math.sin(alpha) * Math.cos(beta);
const y = target.y - radius * Math.sin(beta);
const z = target.z - radius * Math.cos(alpha) * Math.cos(beta);
camera.position = new SPLAT.Vector3(x, y, z);
const direction = target.subtract(camera.position).normalize();
const rx = Math.asin(-direction.y);
const ry = Math.atan2(direction.x, direction.z);
camera.rotation = SPLAT.Quaternion.FromEuler(new SPLAT.Vector3(rx, ry, 0));
isUpdatingCamera = false;
};
const preventDefault = (e: Event) => {
e.preventDefault();
e.stopPropagation();
};
this.dispose = () => {
canvas.removeEventListener("dragenter", preventDefault);
canvas.removeEventListener("dragover", preventDefault);
canvas.removeEventListener("dragleave", preventDefault);
canvas.removeEventListener("contextmenu", preventDefault);
canvas.removeEventListener("mousedown", onMouseDown);
canvas.removeEventListener("mousemove", onMouseMove);
canvas.removeEventListener("wheel", onWheel);
canvas.removeEventListener("touchstart", onTouchStart);
canvas.removeEventListener("touchend", onTouchEnd);
canvas.removeEventListener("touchmove", onTouchMove);
};
canvas.addEventListener("dragenter", preventDefault);
canvas.addEventListener("dragover", preventDefault);
canvas.addEventListener("dragleave", preventDefault);
canvas.addEventListener("contextmenu", preventDefault);
canvas.addEventListener("mousedown", onMouseDown);
canvas.addEventListener("mouseup", onMouseUp);
canvas.addEventListener("mousemove", onMouseMove);
canvas.addEventListener("wheel", onWheel);
canvas.addEventListener("touchstart", onTouchStart);
canvas.addEventListener("touchend", onTouchEnd);
canvas.addEventListener("touchmove", onTouchMove);
this.update();
}
}
export { OrbitControls };