CAD/client/src/helpers/ThreeHintDisplay.ts

127 lines
3.7 KiB
TypeScript

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<string, ThreeHint> = {};
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);
}
}