UNPKG

@inweb/viewer-three

Version:

JavaScript library for rendering CAD and BIM files in a browser using Three.js

283 lines (228 loc) 9.47 kB
/////////////////////////////////////////////////////////////////////////////// // 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, Raycaster, Object3D, MathUtils } from "three"; interface WalkControlsEventMap { change: { type: "change" }; walkspeedchange: { type: "walkspeedchange"; data: number }; } export class WalkControls extends Controls<WalkControlsEventMap> { readonly EYE_HEIGHT = 1.7; readonly FAILING_DISTANCE = 2; readonly GROUND_FOLLOWING_SPEED = 0.05; readonly LOOK_SPEED = 0.1; readonly WALK_SPEED_DELIMITER = 4; readonly WHEEL_SPEED_DELIMITER = 15000; public movementSpeed = 0.1; public multiplier = 3; private raycaster: Raycaster; private groundObjects: Object3D[]; private moveKeys: Set<string>; private moveWheel = 0; private moveClock: Clock; private quaternion: Quaternion; private downPosition: Vector2; private mouseDragOn = false; public rotateDelta: Vector2; private camera: Camera; constructor(camera: Camera, canvas: HTMLElement, groundObjects: Object3D[]) { super(camera, canvas); this.camera = camera; this.groundObjects = groundObjects; this.raycaster = new Raycaster(); this.raycaster.near = 0; this.raycaster.far = this.EYE_HEIGHT + this.FAILING_DISTANCE; 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: "walkspeedchange", data: this.multiplier }); } break; case "NumpadAdd": case "Equal": if (this.multiplier < 10) { this.multiplier = this.multiplier + 1; this.dispatchEvent({ type: "walkspeedchange", 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(); }; private updateGroundFollowing() { const up = new Vector3().copy(this.camera.up); this.raycaster.set(this.object.position, up.negate()); this.raycaster.params = this.raycaster.params = { Mesh: {}, Line: { threshold: 0 }, Line2: { threshold: 0 }, LOD: { threshold: 0 }, Points: { threshold: 0 }, Sprite: { threshold: 0 }, }; const intersects = this.raycaster.intersectObjects(this.groundObjects, false); if (intersects.length > 0) { const groundY = intersects[0].point.y; const targetY = groundY + this.EYE_HEIGHT; // Smoothly interpolate the camera's y position to the target height this.object.position.y = MathUtils.lerp(this.object.position.y, targetY, this.GROUND_FOLLOWING_SPEED); } } override update() { let moved = false; let upgradeGroundFollowing = false; const forward = new Vector3(); const sideways = new Vector3(); if (this.moveKeys.size > 0) { upgradeGroundFollowing = true; const timeDelta = this.moveClock.getDelta(); const moveDelta = (timeDelta * this.multiplier * this.movementSpeed) / this.WALK_SPEED_DELIMITER; this.object.getWorldDirection(forward); forward.y = 0; forward.normalize(); sideways.setFromMatrixColumn(this.object.matrix, 0); sideways.y = 0; sideways.normalize(); if (this.moveKeys.has("KeyW")) { this.object.position.addScaledVector(forward, moveDelta); } if (this.moveKeys.has("KeyS")) { this.object.position.addScaledVector(forward, -moveDelta); } if (this.moveKeys.has("KeyA")) { this.object.position.addScaledVector(sideways, -moveDelta); } if (this.moveKeys.has("KeyD")) { this.object.position.addScaledVector(sideways, moveDelta); } if (this.moveKeys.has("KeyQ")) { this.object.translateY(moveDelta); upgradeGroundFollowing = false; } if (this.moveKeys.has("KeyE")) { this.object.translateY(-moveDelta); upgradeGroundFollowing = false; } const lookDelta = this.LOOK_SPEED + (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; moved = true; } if (this.moveWheel !== 0) { const moveDelta = (this.moveWheel * this.multiplier * this.movementSpeed) / this.WHEEL_SPEED_DELIMITER; this.object.getWorldDirection(forward); forward.y = 0; forward.normalize(); this.object.position.addScaledVector(forward, -moveDelta); this.moveWheel += -1 * Math.sign(this.moveWheel); moved = true; } if (upgradeGroundFollowing) this.updateGroundFollowing(); if (moved) { 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); } }