UNPKG

@inweb/viewer-three

Version:

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

286 lines (234 loc) 9.94 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. /////////////////////////////////////////////////////////////////////////////// // ===================== AI-CODE-FILE ====================== // Source: Claude Sonnet 4.5 // Date: 2025-10-07 // Reviewer: vitaly.ivanov@opendesign.com // Issue: CLOUD-5851 // Notes: Originally AI-generated, modified manually // ========================================================= import { Camera, Clock, Controls, Vector2, Vector3, Raycaster, Object3D, MathUtils } from "three"; interface JoyStickControlsEventMap { change: { type: "change" }; } export class JoyStickControls extends Controls<JoyStickControlsEventMap> { readonly EYE_HEIGHT = 1.7; readonly FAILING_DISTANCE = 2; readonly GROUND_FOLLOWING_SPEED = 0.05; readonly WALK_SPEED_DELIMITER = 4; public movementSpeed = 0.1; public multiplier = 3; private raycaster: Raycaster; private groundObjects: Object3D[]; private canvas: HTMLCanvasElement; private overlayElement: HTMLDivElement; private joyStickCanvas: HTMLCanvasElement; private context: CanvasRenderingContext2D; private moveClock: Clock; private camera: Camera; private joyStickPosition: Vector2; private isActive: boolean = false; private readonly MAX_JOYSTICK_DISTANCE = 100; private readonly INTERNAL_RADIUS = 35; private readonly MAX_MOVE_STICK = 40; private readonly EXTERNAL_RADIUS = 65; private readonly CANVAS_SIZE = 200; private centerX: number; private centerY: number; private pressed: boolean = false; constructor(camera: Camera, domElement: HTMLElement, canvasElement: HTMLCanvasElement, groundObjects: Object3D[]) { super(camera, domElement); this.camera = camera; this.canvas = canvasElement; this.moveClock = new Clock(false); this.joyStickPosition = new Vector2(0, 0); this.groundObjects = groundObjects; this.raycaster = new Raycaster(); this.raycaster.near = 0; this.raycaster.far = this.EYE_HEIGHT + this.FAILING_DISTANCE; this.centerX = this.CANVAS_SIZE / 2; this.centerY = this.CANVAS_SIZE / 2; this.overlayElement = document.createElement("div"); this.overlayElement.id = "joyStickDiv"; this.overlayElement.style.background = "rgba(0,0,0,0)"; this.overlayElement.style.position = "fixed"; this.overlayElement.style.zIndex = "0"; this.overlayElement.style.touchAction = "none"; canvasElement.parentElement.appendChild(this.overlayElement); this.joyStickCanvas = document.createElement("canvas"); this.joyStickCanvas.id = "joyStickCanvas"; this.joyStickCanvas.width = this.CANVAS_SIZE; this.joyStickCanvas.height = this.CANVAS_SIZE; this.overlayElement.appendChild(this.joyStickCanvas); this.context = this.joyStickCanvas.getContext("2d")!; this.joyStickCanvas.addEventListener("pointerdown", this.onPointerDown); document.addEventListener("pointermove", this.onPointerMove); document.addEventListener("pointerup", this.onPointerUp); window.addEventListener("resize", this.onResize); document.addEventListener("fullscreenchange", this.onResize); this.updateVisibility(); this.updatePosition(); this.draw(); } override dispose() { this.joyStickCanvas.removeEventListener("pointerdown", this.onPointerDown); document.removeEventListener("pointermove", this.onPointerMove); document.removeEventListener("pointerup", this.onPointerUp); window.removeEventListener("resize", this.onResize); document.removeEventListener("fullscreenchange", this.onResize); this.overlayElement.remove(); super.dispose(); } private onPointerDown = (event: PointerEvent) => { event.preventDefault(); this.pressed = true; }; private onPointerMove = (event: PointerEvent) => { event.preventDefault(); if (!this.pressed) return; let movedX = event.pageX; let movedY = event.pageY; if ( this.joyStickCanvas.offsetParent && (this.joyStickCanvas.offsetParent as HTMLElement).tagName.toUpperCase() === "BODY" ) { movedX -= this.joyStickCanvas.offsetLeft; movedY -= this.joyStickCanvas.offsetTop; } else if (this.joyStickCanvas.offsetParent) { movedX -= (this.joyStickCanvas.offsetParent as HTMLElement).offsetLeft; movedY -= (this.joyStickCanvas.offsetParent as HTMLElement).offsetTop; } const x = 100 * ((movedX - this.centerX) / this.MAX_MOVE_STICK); const y = 100 * ((movedY - this.centerY) / this.MAX_MOVE_STICK) * -1; const distance = Math.sqrt(x * x + y * y); if (distance > 20) { this.joyStickPosition.set(x, y); this.isActive = true; } else { this.joyStickPosition.set(0, 0); this.isActive = false; } this.draw(); this.moveClock.start(); this.update(); }; private onPointerUp = (event: PointerEvent) => { event.preventDefault(); this.pressed = false; this.joyStickPosition.set(0, 0); this.isActive = false; this.moveClock.stop(); this.draw(); }; private onResize = () => { this.updateVisibility(); this.updatePosition(); }; private updateVisibility() { const isMobile = /Android|webOS|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini/i.test(navigator.userAgent); const isNarrowScreen = window.innerWidth < 1024; if (isMobile || isNarrowScreen) { this.overlayElement.style.display = "block"; } else { this.overlayElement.style.display = "none"; } } private updatePosition() { const rect = this.canvas.getBoundingClientRect(); this.overlayElement.style.top = `${rect.height - this.CANVAS_SIZE}px`; this.overlayElement.style.left = `${rect.left}px`; this.overlayElement.style.width = `${this.CANVAS_SIZE}px`; this.overlayElement.style.height = `${this.CANVAS_SIZE}px`; } private draw() { this.context.clearRect(0, 0, this.CANVAS_SIZE, this.CANVAS_SIZE); // Draw external circle this.context.beginPath(); this.context.arc(this.centerX, this.centerY, this.EXTERNAL_RADIUS, 0, 2 * Math.PI, false); this.context.lineWidth = 2; this.context.strokeStyle = "#35436E"; this.context.globalAlpha = 0.5; this.context.stroke(); // Draw internal circle let movedX = this.centerX + (this.joyStickPosition.x * this.MAX_MOVE_STICK) / 100; let movedY = this.centerY - (this.joyStickPosition.y * this.MAX_MOVE_STICK) / 100; movedX = Math.max(this.MAX_MOVE_STICK, Math.min(this.CANVAS_SIZE - this.MAX_MOVE_STICK, movedX)); movedY = Math.max(this.MAX_MOVE_STICK, Math.min(this.CANVAS_SIZE - this.MAX_MOVE_STICK, movedY)); this.context.beginPath(); this.context.arc(movedX, movedY, this.INTERNAL_RADIUS, 0, 2 * Math.PI, false); this.context.fillStyle = "#35436E"; this.context.lineWidth = 2; this.context.strokeStyle = "#35436E"; this.context.globalAlpha = 0.5; this.context.fill(); this.context.stroke(); } private updateGroundFollowing() { const up = new Vector3().copy(this.camera.up); this.raycaster.set(this.camera.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.camera.position.y = MathUtils.lerp(this.camera.position.y, targetY, this.GROUND_FOLLOWING_SPEED); } } override update() { if (!this.isActive) return; const forwardInput = this.joyStickPosition.y / this.MAX_JOYSTICK_DISTANCE; const rightInput = this.joyStickPosition.x / this.MAX_JOYSTICK_DISTANCE; const timeDelta = this.moveClock.getDelta(); const moveDelta = (timeDelta * this.multiplier * this.movementSpeed) / this.WALK_SPEED_DELIMITER; const forward = new Vector3(); const sideways = new Vector3(); this.camera.getWorldDirection(forward); if (this.groundObjects.length > 0) { forward.y = 0; } forward.normalize(); sideways.setFromMatrixColumn(this.camera.matrix, 0); if (this.groundObjects.length > 0) { sideways.y = 0; } sideways.normalize(); if (forwardInput !== 0) { this.camera.position.addScaledVector(forward, moveDelta * forwardInput); } if (rightInput !== 0) { this.camera.position.addScaledVector(sideways, moveDelta * rightInput); } if (forwardInput !== 0 || rightInput !== 0) { if (this.groundObjects.length > 0) this.updateGroundFollowing(); this.dispatchEvent({ type: "change" }); } } }