122 lines
3.8 KiB
TypeScript
122 lines
3.8 KiB
TypeScript
import * as THREE from "three";
|
|
import { OrbitControls } from "three/examples/jsm/controls/OrbitControls.js";
|
|
import { RENDER_OPTIONS } from "@config/config_frontend";
|
|
import type { SurfacePointCloudSampleResponse } from "./B04_wf1_Surface_Api_Fetch";
|
|
|
|
export interface SurfacePointCloudViewer {
|
|
root: HTMLElement;
|
|
render: (data: SurfacePointCloudSampleResponse | null) => void;
|
|
dispose: () => void;
|
|
}
|
|
|
|
export function createSurfacePointCloudViewer(): SurfacePointCloudViewer {
|
|
const root = document.createElement("div");
|
|
root.className = "b04-surface-viewer";
|
|
|
|
const canvas = document.createElement("canvas");
|
|
canvas.className = "b04-surface-viewer__canvas";
|
|
root.append(canvas);
|
|
|
|
const renderer = new THREE.WebGLRenderer({
|
|
canvas,
|
|
antialias: RENDER_OPTIONS.antialias,
|
|
});
|
|
renderer.setPixelRatio(Math.min(window.devicePixelRatio, RENDER_OPTIONS.maxPixelRatio));
|
|
|
|
const scene = new THREE.Scene();
|
|
scene.background = new THREE.Color(RENDER_OPTIONS.canvasClearColorLight);
|
|
const camera = new THREE.PerspectiveCamera(
|
|
RENDER_OPTIONS.cameraFov,
|
|
1,
|
|
RENDER_OPTIONS.cameraNear,
|
|
RENDER_OPTIONS.cameraFar,
|
|
);
|
|
const controls = new OrbitControls(camera, canvas);
|
|
controls.enableDamping = true;
|
|
|
|
const grid = new THREE.GridHelper(
|
|
200,
|
|
20,
|
|
RENDER_OPTIONS.gridCenterColor,
|
|
RENDER_OPTIONS.gridLineColor,
|
|
);
|
|
scene.add(grid);
|
|
|
|
let pointsObject: THREE.Points | null = null;
|
|
let animationFrame = 0;
|
|
|
|
function resize(): void {
|
|
const rect = root.getBoundingClientRect();
|
|
const width = Math.max(1, Math.floor(rect.width));
|
|
const height = Math.max(1, Math.floor(rect.height));
|
|
renderer.setSize(width, height, false);
|
|
camera.aspect = width / height;
|
|
camera.updateProjectionMatrix();
|
|
}
|
|
|
|
function animate(): void {
|
|
resize();
|
|
controls.update();
|
|
renderer.render(scene, camera);
|
|
animationFrame = requestAnimationFrame(animate);
|
|
}
|
|
|
|
function clearPoints(): void {
|
|
if (!pointsObject) return;
|
|
pointsObject.geometry.dispose();
|
|
const material = pointsObject.material;
|
|
if (Array.isArray(material)) material.forEach((item) => item.dispose());
|
|
else material.dispose();
|
|
scene.remove(pointsObject);
|
|
pointsObject = null;
|
|
}
|
|
|
|
function render(data: SurfacePointCloudSampleResponse | null): void {
|
|
clearPoints();
|
|
if (!data || data.points.length === 0) return;
|
|
|
|
const bounds = data.bounds;
|
|
const cx = ((bounds.x_min ?? 0) + (bounds.x_max ?? 0)) / 2;
|
|
const cy = ((bounds.y_min ?? 0) + (bounds.y_max ?? 0)) / 2;
|
|
const cz = ((bounds.z_min ?? 0) + (bounds.z_max ?? 0)) / 2;
|
|
const positions = new Float32Array(data.points.length * 3);
|
|
data.points.forEach((point, index) => {
|
|
positions[index * 3] = point[0] - cx;
|
|
positions[index * 3 + 1] = point[2] - cz;
|
|
positions[index * 3 + 2] = point[1] - cy;
|
|
});
|
|
|
|
const geometry = new THREE.BufferGeometry();
|
|
geometry.setAttribute("position", new THREE.BufferAttribute(positions, 3));
|
|
geometry.computeBoundingSphere();
|
|
const material = new THREE.PointsMaterial({
|
|
color: RENDER_OPTIONS.pointColor,
|
|
size: 1.6,
|
|
sizeAttenuation: false,
|
|
});
|
|
pointsObject = new THREE.Points(geometry, material);
|
|
scene.add(pointsObject);
|
|
|
|
const radius = geometry.boundingSphere?.radius ?? 100;
|
|
camera.position.set(radius * 0.9, radius * 0.7, radius * 1.4);
|
|
camera.near = Math.max(RENDER_OPTIONS.cameraNear, radius / 10000);
|
|
camera.far = Math.max(RENDER_OPTIONS.cameraFar, radius * 8);
|
|
camera.updateProjectionMatrix();
|
|
controls.target.set(0, 0, 0);
|
|
controls.update();
|
|
}
|
|
|
|
animationFrame = requestAnimationFrame(animate);
|
|
|
|
return {
|
|
root,
|
|
render,
|
|
dispose: () => {
|
|
cancelAnimationFrame(animationFrame);
|
|
clearPoints();
|
|
controls.dispose();
|
|
renderer.dispose();
|
|
},
|
|
};
|
|
}
|