import React, { useEffect, useRef, useState } from "react"; import * as THREE from "three"; import { OrbitControls } from "three/examples/jsm/controls/OrbitControls.js"; interface Point { x: number; y: number; z: number; value: number | string; // 表达强度或细胞类型 } interface PointCloudProps { data: Point[]; isCategorical?: boolean; // 是否为分类数据(细胞类型) autoRotate?: boolean; // 是否自动旋转 } const PointCloud: React.FC = ({ data, isCategorical = false, autoRotate = false }) => { const containerRef = useRef(null); const rendererRef = useRef(null); const sceneRef = useRef(null); const geometryRef = useRef(null); const materialRef = useRef(null); const animationIdRef = useRef(null); const controlsRef = useRef(null); const [isSpinning, setIsSpinning] = useState(autoRotate); const isSpinningRef = useRef(autoRotate); const checkboxId = useRef(`spin-checkbox-${Math.random().toString(36).substring(2, 11)}`).current; // 为细胞类型生成颜色映射 const generateCellTypeColors = (cellTypes: string[]) => { const colorMap = new Map(); const colors = [ '#FF6B6B', '#4ECDC4', '#45B7D1', '#96CEB4', '#FFEAA7', '#DDA0DD', '#98D8C8', '#F7DC6F', '#BB8FCE', '#85C1E9', '#F8C471', '#82E0AA', '#F1948A', '#85CDCE', '#D7BDE2', '#A9DFBF', '#F9E79F', '#D5A6BD', '#AED6F1', '#E8DAEF' ]; cellTypes.forEach((type, index) => { const colorHex = colors[index % colors.length]; colorMap.set(type, new THREE.Color(colorHex)); }); return colorMap; }; useEffect(() => { if (!containerRef.current || data.length === 0) return; // 确保ref与当前状态同步 isSpinningRef.current = isSpinning; // 清理之前的资源 if (animationIdRef.current !== null) { cancelAnimationFrame(animationIdRef.current); animationIdRef.current = null; } if (rendererRef.current) { rendererRef.current.dispose(); rendererRef.current = null; } if (geometryRef.current) { geometryRef.current.dispose(); geometryRef.current = null; } if (materialRef.current) { materialRef.current.dispose(); materialRef.current = null; } // 清除旧渲染内容 while (containerRef.current.firstChild) { containerRef.current.removeChild(containerRef.current.firstChild); } const width = containerRef.current.clientWidth; const height = containerRef.current.clientHeight; // 初始化场景、相机、渲染器 const scene = new THREE.Scene(); sceneRef.current = scene; // 存储scene引用 const camera = new THREE.PerspectiveCamera(60, width / height, 0.1, 1000); camera.position.set(-15, 15, -12); // 从左下后方观察,使Z轴指向右上方 camera.lookAt(0, 0, 0); // 确保相机朝向原点 const renderer = new THREE.WebGLRenderer({ antialias: true }); renderer.setSize(width, height); rendererRef.current = renderer; containerRef.current.appendChild(renderer.domElement); // 控件 const controls = new OrbitControls(camera, renderer.domElement); controls.enableDamping = true; controlsRef.current = controls; // 坐标轴辅助 const axesHelper = new THREE.AxesHelper(10); scene.add(axesHelper); // 点云数据 const geometry = new THREE.BufferGeometry(); geometryRef.current = geometry; const positions: number[] = []; const colors: number[] = []; const sizes: number[] = []; let colorMap: Map | null = null; let cellTypes: string[] = []; let maxValue = 0; if (isCategorical) { // 分类数据处理 cellTypes = Array.from(new Set(data.map(p => p.value as string))).sort(); colorMap = generateCellTypeColors(cellTypes); } else { // 数值数据处理 maxValue = Math.max(...data.map((p) => p.value as number)); } const color = new THREE.Color(); data.forEach((point) => { positions.push(point.x, point.y, point.z); if (isCategorical && colorMap) { // 分类数据:按细胞类型着色 const cellColor = colorMap.get(point.value as string) || new THREE.Color('#CCCCCC'); colors.push(cellColor.r, cellColor.g, cellColor.b); sizes.push(0.8); // 统一大小 } else { // 数值数据:按表达强度着色 const value = point.value as number; color.setHSL(0.6 * (1 - value / maxValue), 1.0, 0.5); colors.push(color.r, color.g, color.b); sizes.push(0.3 + 1.5 * value / maxValue); } }); geometry.setAttribute("position", new THREE.Float32BufferAttribute(positions, 3)); geometry.setAttribute("color", new THREE.Float32BufferAttribute(colors, 3)); geometry.setAttribute("size", new THREE.Float32BufferAttribute(sizes, 1)); const material = new THREE.PointsMaterial({ vertexColors: true, size: 0.5, sizeAttenuation: true, }); materialRef.current = material; const points = new THREE.Points(geometry, material); scene.add(points); // 点击点检测(raycaster) const raycaster = new THREE.Raycaster(); const mouse = new THREE.Vector2(); const onClick = (event: MouseEvent) => { const rect = renderer.domElement.getBoundingClientRect(); mouse.x = ((event.clientX - rect.left) / rect.width) * 2 - 1; mouse.y = -((event.clientY - rect.top) / rect.height) * 2 + 1; raycaster.setFromCamera(mouse, camera); const intersects = raycaster.intersectObject(points); if (intersects.length > 0) { const i = intersects[0].index ?? 0; const p = data[i]; console.log(`Clicked: x=${p.x}, y=${p.y}, z=${p.z}, value=${p.value}`); if (isCategorical) { alert(`Cell @ (${p.x.toFixed(1)}, ${p.y.toFixed(1)}, ${p.z.toFixed(1)})\nCell Type: ${p.value}`); } else { alert(`Cell @ (${p.x.toFixed(1)}, ${p.y.toFixed(1)}, ${p.z.toFixed(1)})\nExpression: ${(p.value as number).toFixed(3)}`); } } }; renderer.domElement.addEventListener("click", onClick); // 渲染循环 const animate = () => { animationIdRef.current = requestAnimationFrame(animate); // 自动旋转逻辑 - 使用 ref 来避免重新创建场景 if (isSpinningRef.current && autoRotate) { scene.rotation.z += 0.002; // 围绕Z轴更慢速旋转 } controls.update(); renderer.render(scene, camera); }; animate(); // 图例 const legend = document.createElement("div"); legend.className = "point-cloud-legend"; if (isCategorical && colorMap) { // 分类数据图例 let legendHTML = '
细胞类型
'; cellTypes.forEach(type => { const cellColor = colorMap!.get(type); if (cellColor) { const hexColor = `#${cellColor.getHexString()}`; legendHTML += `
${type}
`; } }); legend.innerHTML = legendHTML; } else { // 数值数据图例 legend.innerHTML = `
↑ High
↓ Low
`; } containerRef.current.appendChild(legend); // 清理 return () => { // 停止动画循环 if (animationIdRef.current !== null) { cancelAnimationFrame(animationIdRef.current); animationIdRef.current = null; } // 移除事件监听器 if (rendererRef.current && rendererRef.current.domElement) { rendererRef.current.domElement.removeEventListener("click", onClick); } // 清理控制器 if (controlsRef.current) { controlsRef.current.dispose(); controlsRef.current = null; } // 释放Three.js资源 if (rendererRef.current) { rendererRef.current.dispose(); rendererRef.current = null; } if (geometryRef.current) { geometryRef.current.dispose(); geometryRef.current = null; } if (materialRef.current) { materialRef.current.dispose(); materialRef.current = null; } }; }, [data, isCategorical, autoRotate]); // 监听autoRotate prop变化,重置旋转状态 useEffect(() => { setIsSpinning(autoRotate); isSpinningRef.current = autoRotate; }, [autoRotate]); // 监听isSpinning状态变化,只更新ref,不重新创建场景 useEffect(() => { isSpinningRef.current = isSpinning; }, [isSpinning]); // 组件卸载时的最终清理 useEffect(() => { return () => { if (animationIdRef.current !== null) { cancelAnimationFrame(animationIdRef.current); } if (controlsRef.current) { controlsRef.current.dispose(); } if (rendererRef.current) { rendererRef.current.dispose(); } if (geometryRef.current) { geometryRef.current.dispose(); } if (materialRef.current) { materialRef.current.dispose(); } }; }, []); return (
{autoRotate === true && (
setIsSpinning(e.target.checked)} style={{ cursor: 'pointer', width: '16px', height: '16px', accentColor: '#4ECDC4' }} />
)}
); }; export default PointCloud;