import ky from 'ky'; import type React from 'react'; import { useEffect, useRef, useState } from 'react'; import { Link } from 'react-router-dom'; import * as THREE from 'three'; import { TrackballControls } from 'three/examples/jsm/controls/TrackballControls'; import { useAppDispatch } from '../../app/hooks'; import DataUtils from '../data/DataUtils'; import Loading from '../loading/Loading'; import NotFound from '../not-found/NotFound'; import { setTitle } from '../page-title/pageTitleSlice'; import styles from './DataViewer.module.css'; class Viewer3D { private mount: HTMLDivElement; private frameId: number | null = null; private scene: THREE.Scene | null = null; private camera: THREE.PerspectiveCamera | null = null; private controls: TrackballControls | null = null; private renderer: THREE.WebGLRenderer | null = null; constructor(mount: HTMLDivElement) { this.mount = mount; this.start = this.start.bind(this); this.stop = this.stop.bind(this); this.animate = this.animate.bind(this); } init(data: string) { const lines = data.split('\n'); const head = lines .shift() ?.trim() .split('\t') .map((line) => { return parseInt(line.replace(/^#/g, ''), 10) || 0; }); if (typeof head === 'undefined' || head.length !== 3 || head.includes(0)) { return false; } const width = this.mount.clientWidth; const height = this.mount.clientHeight; const fov = 7.5 + (head[0] * 2.5) / 100.0; this.camera = new THREE.PerspectiveCamera(fov, width / height, 1, 10000); this.camera.position.x = 1300; this.camera.position.y = 1300; this.camera.position.z = 1100; this.camera.up.set(0, 0, 1); this.controls = new TrackballControls(this.camera, this.mount); const obj3d = new THREE.Object3D(); this.scene = new THREE.Scene(); this.scene.add(obj3d); const DrawCube = (width: number, height: number, depth: number) => { const mesh = new THREE.Mesh(new THREE.BoxGeometry(width, height, depth * 2), new THREE.MeshBasicMaterial()); const cube = new THREE.BoxHelper(mesh, 0x000000); return cube; }; obj3d.add(DrawCube(head[0], head[1], head[2])); const material = { e: new THREE.MeshBasicMaterial({ color: 0x0000ff }), i: new THREE.MeshBasicMaterial({ color: 0xff0000 }), g: new THREE.MeshBasicMaterial({ color: 0x00ff00 }), p: new THREE.MeshBasicMaterial({ color: 0xffff00 }), s: new THREE.MeshBasicMaterial({ color: 0xff00ff }), ps: new THREE.MeshBasicMaterial({ color: 0x00ffff }), }; const geometory = new THREE.SphereGeometry(4, 10, 10); lines.forEach((line) => { const point = line.trim().split('\t'); if (point.length === 4 && Object.hasOwn(material, point[0])) { const particle = new THREE.Mesh(geometory, material[point[0] as 'e' | 'i' | 'g' | 'p' | 's' | 'ps']); particle.position.x = parseFloat(point[1]) - head[0] / 2.0; particle.position.y = parseFloat(point[2]) - head[1] / 2.0; particle.position.z = head[2] - parseFloat(point[3]) * 2.0; obj3d.add(particle); } }); try { this.renderer = new THREE.WebGLRenderer({ antialias: true }); this.renderer.setSize(width, height); this.renderer.setClearColor(0xffffff); this.mount.appendChild(this.renderer.domElement); } catch { return false; } return true; } start() { this.frameId = requestAnimationFrame(this.animate); } stop() { if (this.frameId) cancelAnimationFrame(this.frameId); if (this.renderer && this.mount) this.mount.removeChild(this.renderer.domElement); } animate() { if (this.controls) this.controls.update(); if (this.renderer) this.renderer.clear(); if (this.renderer && this.scene && this.camera) this.renderer.render(this.scene, this.camera); this.frameId = requestAnimationFrame(this.animate); } } interface DataViewerProps { name: string; } const DataViewer: React.FC = ({ name: initialName }) => { const dispatch = useAppDispatch(); const [name, setName] = useState(initialName); const [data, setData] = useState(''); const mountRef = useRef(null); const viewerRef = useRef(null); const isActiveRef = useRef(true); useEffect(() => { isActiveRef.current = true; if (!DataUtils.exists(name)) { if (name !== '') { // eslint-disable-next-line react-hooks/set-state-in-effect -- intentional validation reset setName(''); } return; } const controller = new AbortController(); ky.get(`/data/${name}.dat`, { signal: controller.signal }) .text() .then((data) => { if (isActiveRef.current) { dispatch(setTitle(`View 3D: ${name}`)); setData(data); } }) .catch(() => { if (isActiveRef.current && !controller.signal.aborted) { setName(''); } }); return () => { controller.abort(); }; }, [name, dispatch]); useEffect(() => { if (mountRef.current && data !== '') { // Stop and clean up the old viewer if switching datasets if (viewerRef.current) { viewerRef.current.stop(); viewerRef.current = null; } const viewer = new Viewer3D(mountRef.current); if (viewer.init(data)) { viewerRef.current = viewer; viewerRef.current.start(); } else { setName('@@NOWEBGL@@'); } } }, [data]); useEffect(() => { return () => { isActiveRef.current = false; if (viewerRef.current) { viewerRef.current.stop(); viewerRef.current = null; } }; }, []); switch (name) { case '': return ; case '@@NOWEBGL@@': return
It does not appear your computer supports WebGL.
; } if (data === '') { return ; } return (
Back to {name}
); }; export default DataViewer;