import * as THREE from "three"; export type ThreeHint = { mesh: THREE.Mesh, position: THREE.Vector3Like, options: ThreeHintOptions, } export type ThreeBaseHintOptions = { color: THREE.ColorRepresentation, } export type ThreePointHintOptions = ThreeBaseHintOptions & { kind: 'point', size: number, } export type ThreeCircleHintOptions = ThreeBaseHintOptions & { kind: 'circle', radius: number, thickness: number, } export type ThreeHintOptions = ThreePointHintOptions | ThreeCircleHintOptions; export class ThreeHintDisplay { private scene: THREE.Scene; private camera: THREE.PerspectiveCamera | THREE.OrthographicCamera; private renderer: THREE.WebGLRenderer; private readonly baseMaterial = new THREE.MeshBasicMaterial({ color: 'red' }); private readonly hints: Record = {}; constructor( scene: THREE.Scene, camera: THREE.PerspectiveCamera | THREE.OrthographicCamera, renderer: THREE.WebGLRenderer, ) { this.scene = scene; this.camera = camera; this.renderer = renderer; } private createGeometry(options: ThreeHintOptions): THREE.BufferGeometry { switch (options.kind) { case 'point': return new THREE.SphereGeometry(1); case 'circle': return new THREE.TorusGeometry(options.radius, options.thickness * options.radius); default: throw new Error('Unknown volatile geometry type'); } } private ensure(id: string, options: ThreeHintOptions): ThreeHint { if (!this.hints[id]) { const material = this.baseMaterial.clone(); material.color.set(options.color); this.hints[id] = { mesh: new THREE.Mesh( this.createGeometry(options), material, ), position: { x: 0, y: 0, z: 0 }, options, }; this.scene.add(this.hints[id].mesh); } return this.hints[id]; } private disposeHint(id: string) { const point = this.hints[id]; if (point) { this.scene.remove(point.mesh); point.mesh.geometry.dispose(); delete (this.hints[id]); } } public dispose() { for (const id in this.hints) this.disposeHint(id); } public set(id: string, position: THREE.Vector3Like, options: ThreeHintOptions) { const object = this.ensure(id, options); object.position = position; this.applyCameraToHint(object); } private applyCameraToHint(hint: ThreeHint) { const rendererSize = new THREE.Vector2(); this.renderer.getSize(rendererSize); // additional actions switch (hint.options.kind) { case 'point': let scale: number; if (this.camera instanceof THREE.PerspectiveCamera) { const distance = this.camera.position.distanceTo(hint.position); const fovRad = THREE.MathUtils.degToRad(this.camera.fov); scale = (hint.options.size * distance * Math.tan(fovRad / 2)) / (rendererSize.height / 2); } else { scale = (hint.options.size * (this.camera.top - this.camera.bottom)) / rendererSize.height; } hint.mesh.scale.setScalar(scale); break; case 'circle': hint.mesh.lookAt(this.camera.position); break; } hint.mesh.position.copy(hint.position); } public applyCamera() { for (const hint of Object.values(this.hints)) this.applyCameraToHint(hint); } }