import { useEffect, useRef } from "react"; import * as THREE from "three"; import * as BufferGeometryUtils from "three/examples/jsm/utils/BufferGeometryUtils.js"; import { OrbitControls } from "three/examples/jsm/controls/OrbitControls"; import { PlanetInfo } from "@/types"; const commandCenterIds = [2254, 2524, 2525, 2533, 2534, 2549, 2550, 2551]; export interface ExtractorHead { parent_id: number; head_id: number; latitude: number; longitude: number; } const PinsCanvas3D = ({ planetInfo, }: { planetInfo: PlanetInfo | undefined; }) => { const animationRef = useRef(0); useEffect(() => { if (!planetInfo || planetInfo.pins.length === 0) return; const pins = planetInfo?.pins ?? []; const extractors: ExtractorHead[] = []; planetInfo.pins.forEach((p) => { if ( !p.extractor_details?.heads || p.extractor_details?.heads.length === 0 ) return; p.extractor_details?.heads.forEach((h) => { if (!h) return; extractors.push({ ...h, parent_id: p.pin_id }); }); }); const CANVAS = document.querySelector("#canvas") as HTMLCanvasElement; if (!CANVAS) return; const SCENE_ANTIALIAS = true; const SCENE_ALPHA = true; const SCENE_BACKGROUND_COLOR = 0x000000; const CAMERA_FOV = 20; const CAMERA_NEAR = 10; const CAMERA_FAR = 4000; const SPHERE_RADIUS = 30; const LATITUDE_COUNT = 40; const LONGITUDE_COUNT = 80; const DOT_SIZE = 0.2; const DOT_SIZE_EXTRACTOR = 0.4; const DOT_COLOR_GLOBE = 0x36454f; const DOT_COLOR_STRUCTURE = 0xfdda0d; const DOT_COLOR_HEAD = 0x00ffff; const DOT_COLOR_CC = 0xff8080; const renderScene = () => { const renderer = new THREE.WebGLRenderer({ canvas: CANVAS as HTMLCanvasElement, antialias: SCENE_ANTIALIAS, alpha: SCENE_ALPHA, }); const scene = new THREE.Scene(); scene.background = new THREE.Color(SCENE_BACKGROUND_COLOR); const dotGeometries: THREE.CircleGeometry[] = []; const dotGeometriesPI: THREE.CircleGeometry[] = []; const dotGeometriesHead: THREE.CircleGeometry[] = []; const dotGeometriesCommandCenter: THREE.CircleGeometry[] = []; const vector = new THREE.Vector3(); const vectorPI = new THREE.Vector3(); const vectorHead = new THREE.Vector3(); const vectorCommandCenter = new THREE.Vector3(); extractors.forEach((h) => { const dotGeometryHead = new THREE.CircleGeometry(DOT_SIZE_EXTRACTOR, 9); const phi = h.latitude; const theta = h.longitude; vectorHead.setFromSphericalCoords(SPHERE_RADIUS, phi, theta); dotGeometryHead.lookAt(vectorHead); dotGeometryHead.translate(vectorHead.x, vectorHead.y, vectorHead.z + 1); if (!dotGeometryHead) return; dotGeometriesHead.push(dotGeometryHead); }); pins .filter((p) => !commandCenterIds.some((c) => c === p.type_id)) .forEach((p) => { const dotGeometryPI = new THREE.CircleGeometry(DOT_SIZE, 9); const phi = p.latitude; const theta = p.longitude; vectorPI.setFromSphericalCoords(SPHERE_RADIUS, phi, theta); dotGeometryPI.lookAt(vectorPI); dotGeometryPI.translate(vectorPI.x, vectorPI.y, vectorPI.z); dotGeometriesPI.push(dotGeometryPI); }); pins .filter((p) => commandCenterIds.some((c) => c === p.type_id)) .forEach((p) => { const dotGeometryCommandCenter = new THREE.CircleGeometry( DOT_SIZE, 9, ); const phi = p.latitude; const theta = p.longitude; vectorCommandCenter.setFromSphericalCoords(SPHERE_RADIUS, phi, theta); dotGeometryCommandCenter.lookAt(vectorCommandCenter); dotGeometryCommandCenter.translate( vectorCommandCenter.x, vectorCommandCenter.y, vectorCommandCenter.z, ); dotGeometriesCommandCenter.push(dotGeometryCommandCenter); }); for (let lat = 0; lat < LATITUDE_COUNT; lat += 1) { for (let lng = 0; lng < LONGITUDE_COUNT; lng += 1) { const dotGeometry = new THREE.CircleGeometry(DOT_SIZE, 5); const phi = (Math.PI / LATITUDE_COUNT) * lat; const theta = ((2 * Math.PI) / LONGITUDE_COUNT) * lng; vector.setFromSphericalCoords(SPHERE_RADIUS, phi, theta); dotGeometry.lookAt(vector); dotGeometry.translate(vector.x, vector.y, vector.z); if (!dotGeometry) return; dotGeometries.push(dotGeometry); } } const mergedDotGeometries = BufferGeometryUtils.mergeGeometries(dotGeometries); const mergedDotGeometriesPI = BufferGeometryUtils.mergeGeometries(dotGeometriesPI); const mergedDotGeometriesHead = BufferGeometryUtils.mergeGeometries(dotGeometriesHead); const mergedDotGeometriesCC = BufferGeometryUtils.mergeGeometries( dotGeometriesCommandCenter, ); const dotMaterial = new THREE.MeshBasicMaterial({ color: DOT_COLOR_GLOBE, side: THREE.DoubleSide, wireframe: true, }); const dotMaterialPI = new THREE.MeshBasicMaterial({ color: DOT_COLOR_STRUCTURE, side: THREE.DoubleSide, }); const dotMaterialHead = new THREE.MeshBasicMaterial({ color: DOT_COLOR_HEAD, side: THREE.DoubleSide, }); const dotMaterialCC = new THREE.MeshBasicMaterial({ color: DOT_COLOR_CC, side: THREE.DoubleSide, }); const dotMesh = new THREE.Mesh(mergedDotGeometries, dotMaterial); const dotMeshPI = new THREE.Mesh(mergedDotGeometriesPI, dotMaterialPI); const dotMeshHead = new THREE.Mesh( mergedDotGeometriesHead, dotMaterialHead, ); const dotMeshCC = new THREE.Mesh(mergedDotGeometriesCC, dotMaterialCC); scene.add(dotMesh); scene.add(dotMeshPI); scene.add(dotMeshHead); scene.add(dotMeshCC); const camera = new THREE.PerspectiveCamera( CAMERA_FOV, CANVAS.width / CANVAS.height, CAMERA_NEAR, CAMERA_FAR, ); camera.position.set(vectorPI.x, vectorPI.y, vectorPI.z); const controls = new OrbitControls(camera, renderer.domElement); controls.maxDistance = 300; controls.minDistance = 60; controls.update(); const animate = (time: number) => { time *= 0.001; controls.update(); renderer.render(scene, camera); animationRef.current = requestAnimationFrame(animate); }; animationRef.current = requestAnimationFrame(animate); }; const setCanvasSize = () => { CANVAS.width = window.innerWidth; CANVAS.height = window.innerHeight; renderScene(); }; setCanvasSize(); window.addEventListener("resize", setCanvasSize); return () => cancelAnimationFrame(animationRef.current); }, [planetInfo]); return ; }; export default PinsCanvas3D;