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(); }, }; }