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 };