@inweb/viewer-three
Version:
JavaScript library for rendering CAD and BIM files in a browser using Three.js
206 lines (164 loc) • 7.23 kB
text/typescript
///////////////////////////////////////////////////////////////////////////////
// Copyright (C) 2002-2025, Open Design Alliance (the "Alliance").
// All rights reserved.
//
// This software and its documentation and related materials are owned by
// the Alliance. The software may only be incorporated into application
// programs owned by members of the Alliance, subject to a signed
// Membership Agreement and Supplemental Software License Agreement with the
// Alliance. The structure and organization of this software are the valuable
// trade secrets of the Alliance and its suppliers. The software is also
// protected by copyright law and international treaty provisions. Application
// programs incorporating this software must include the following statement
// with their copyright notices:
//
// This application incorporates Open Design Alliance software pursuant to a
// license agreement with Open Design Alliance.
// Open Design Alliance Copyright (C) 2002-2025 by Open Design Alliance.
// All rights reserved.
//
// By use of this software, its documentation or related materials, you
// acknowledge and accept the above terms.
///////////////////////////////////////////////////////////////////////////////
import { Clock, Camera, Controls, Quaternion, Vector2, Vector3 } from "three";
interface FlyControlsEventMap {
change: { type: "change" };
flyspeedchange: { type: "flyspeedchange"; data: number };
}
export class FlyControls extends Controls<FlyControlsEventMap> {
public movementSpeed = 0.2;
public lookSpeed = 5;
public multiplier = 5;
private moveKeys: Set<string>;
private moveWheel = 0;
private moveClock: Clock;
private quaternion: Quaternion;
private downPosition: Vector2;
private mouseDragOn = false;
public rotateDelta: Vector2;
constructor(camera: Camera, canvas: HTMLElement) {
super(camera, canvas);
this.moveKeys = new Set();
this.moveClock = new Clock();
this.quaternion = camera.quaternion.clone();
this.downPosition = new Vector2(0, 0);
this.rotateDelta = new Vector2(0, 0);
this.domElement.addEventListener("pointerdown", this.onPointerDown);
this.domElement.addEventListener("pointermove", this.onPointerMove);
this.domElement.addEventListener("pointerup", this.onPointerUp);
this.domElement.addEventListener("pointercancel", this.onPointerCancel);
this.domElement.addEventListener("wheel", this.onWheel);
window.addEventListener("keydown", this.onKeyDown);
window.addEventListener("keyup", this.onKeyUp);
}
override dispose() {
this.domElement.removeEventListener("pointerdown", this.onPointerDown);
this.domElement.removeEventListener("pointermove", this.onPointerMove);
this.domElement.removeEventListener("pointerup", this.onPointerUp);
this.domElement.removeEventListener("pointercancel", this.onPointerCancel);
this.domElement.removeEventListener("wheel", this.onWheel);
window.removeEventListener("keydown", this.onKeyDown);
window.removeEventListener("keyup", this.onKeyUp);
super.dispose();
}
onPointerDown = (event: PointerEvent) => {
if (event.button !== 0) return;
this.domElement.setPointerCapture(event.pointerId);
this.downPosition.set(event.clientX, event.clientY);
this.quaternion.copy(this.object.quaternion);
this.mouseDragOn = true;
};
onPointerMove = (event: PointerEvent) => {
if (!this.mouseDragOn) return;
const movePosition = new Vector2(event.clientX, event.clientY);
if (this.downPosition.distanceTo(movePosition) === 0) return;
this.rotateDelta.copy(this.downPosition).sub(movePosition);
this.rotateCamera(this.rotateDelta);
this.dispatchEvent({ type: "change" });
};
onPointerUp = (event: PointerEvent) => {
this.domElement.releasePointerCapture(event.pointerId);
this.mouseDragOn = false;
};
onPointerCancel = (event: PointerEvent) => {
this.domElement.dispatchEvent(new PointerEvent("pointerup", event));
};
onWheel = (event: WheelEvent) => {
this.moveWheel = event.deltaY;
this.update();
};
onKeyDown = (event: KeyboardEvent) => {
switch (event.code) {
case "NumpadSubtract":
case "Minus":
if (this.multiplier > 1) {
this.multiplier = this.multiplier - 1;
this.dispatchEvent({ type: "flyspeedchange", data: this.multiplier });
}
break;
case "NumpadAdd":
case "Equal":
if (this.multiplier < 10) {
this.multiplier = this.multiplier + 1;
this.dispatchEvent({ type: "flyspeedchange", data: this.multiplier });
}
break;
case "ArrowLeft":
case "ArrowRight":
case "ArrowUp":
case "ArrowDown":
case "KeyW":
case "KeyS":
case "KeyA":
case "KeyD":
case "KeyQ":
case "KeyE":
this.moveKeys.add(event.code);
this.update();
break;
}
};
onKeyUp = (event: KeyboardEvent) => {
if (this.moveKeys.delete(event.code)) this.update();
};
override update() {
if (this.moveKeys.size > 0) {
const timeDelta = this.moveClock.getDelta();
const moveDelta = timeDelta * this.movementSpeed * this.multiplier;
if (this.moveKeys.has("KeyW")) this.object.translateZ(-moveDelta);
if (this.moveKeys.has("KeyS")) this.object.translateZ(moveDelta);
if (this.moveKeys.has("KeyA")) this.object.translateX(-moveDelta);
if (this.moveKeys.has("KeyD")) this.object.translateX(moveDelta);
if (this.moveKeys.has("KeyQ")) this.object.translateY(moveDelta);
if (this.moveKeys.has("KeyE")) this.object.translateY(-moveDelta);
const lookDelta = this.lookSpeed + (this.multiplier - 1);
if (this.moveKeys.has("ArrowUp")) this.rotateCamera(this.rotateDelta.add(new Vector2(0, -lookDelta / 2)));
if (this.moveKeys.has("ArrowDown")) this.rotateCamera(this.rotateDelta.add(new Vector2(0, lookDelta / 2)));
if (this.moveKeys.has("ArrowLeft")) this.rotateCamera(this.rotateDelta.add(new Vector2(lookDelta, 0)));
if (this.moveKeys.has("ArrowRight")) this.rotateCamera(this.rotateDelta.add(new Vector2(-lookDelta, 0)));
this.moveWheel = 0;
this.dispatchEvent({ type: "change" });
}
if (this.moveWheel !== 0) {
const moveDelta = this.moveWheel * 0.0001 * this.movementSpeed * this.multiplier;
this.object.translateZ(-moveDelta);
this.moveWheel += -1 * Math.sign(this.moveWheel);
this.dispatchEvent({ type: "change" });
}
if (this.moveKeys.size === 0 && this.moveWheel === 0) {
this.moveClock.stop();
this.moveClock.autoStart = true;
}
}
rotateCamera(delta: Vector2) {
const rotateX = (Math.PI * delta.x) / this.domElement.clientWidth;
const rotateY = (Math.PI * delta.y) / this.domElement.clientHeight;
const xRotation = new Quaternion();
xRotation.setFromAxisAngle(this.object.up, rotateX);
const yRotation = new Quaternion();
yRotation.setFromAxisAngle(new Vector3(1, 0, 0), rotateY);
const quaternion = this.quaternion.clone();
quaternion.premultiply(xRotation).multiply(yRotation).normalize();
this.object.setRotationFromQuaternion(quaternion);
}
}