import { createSignal, onMount, onCleanup, Show, createEffect, } from "solid-js"; import * as THREE from "three"; import { ThreeMFLoader } from "three/addons/loaders/3MFLoader.js"; export interface TokenViewerProps { url: string | null; } export default function MdTokenViewer(props: TokenViewerProps) { const [viewerRef, setViewerRef] = createSignal(null); const [isLoaded, setIsLoaded] = createSignal(false); const [error, setError] = createSignal(null); let scene: THREE.Scene | null = null; let camera: THREE.PerspectiveCamera | null = null; let renderer: THREE.WebGLRenderer | null = null; let mesh: THREE.Mesh | null = null; let group: THREE.Group | null = null; let animationId: number | null = null; let isDragging = false; let previousMousePosition = { x: 0, y: 0 }; // 加载 3MF 用于预览 const load3mf = async (url: string) => { const viewerEl = viewerRef(); if (!viewerEl) return; // 清理旧的场景 if (group) { scene?.remove(group); group.traverse((obj) => { if ((obj as THREE.Mesh).isMesh) { const meshObj = obj as THREE.Mesh; meshObj.geometry.dispose(); (meshObj.material as THREE.Material).dispose(); } }); group = null; mesh = null; } try { const loader = new ThreeMFLoader(); const object = await loader.loadAsync(url); // 3MF 文件可能返回一个 Group,包含多个 Mesh group = object instanceof THREE.Group ? object : new THREE.Group().add(object); group.rotateX(Math.PI); // 为每个 mesh 启用原始颜色 group.traverse((child) => { if ((child as THREE.Mesh).isMesh) { const childMesh = child as THREE.Mesh; const material = childMesh.material as THREE.MeshStandardMaterial; // 保留原始顶点颜色或材质颜色 if (childMesh.geometry.attributes.color) { material.vertexColors = true; } // 确保材质是标准材质以支持光照 if (!(childMesh.material instanceof THREE.MeshStandardMaterial)) { childMesh.material = new THREE.MeshStandardMaterial({ color: material.color, metalness: 0.3, roughness: 0.7, }); } } }); scene?.add(group); mesh = group.children[0] as THREE.Mesh; // 调整相机 if (camera && scene) { const box = new THREE.Box3().setFromObject(group); const size = box.getSize(new THREE.Vector3()); const maxDim = Math.max(size.x, size.y, size.z); camera.position.set(maxDim * 2, maxDim * 2, maxDim * 2); camera.lookAt(0, 0, 0); } setIsLoaded(true); } catch (e) { console.error("加载 3MF 预览失败:", e); setError(e instanceof Error ? e.message : "加载模型失败"); } }; // 初始化 Three.js 场景 onMount(() => { const viewerEl = viewerRef(); if (!viewerEl) return; // 创建场景 scene = new THREE.Scene(); scene.background = new THREE.Color(0xf3f4f6); // 创建相机 camera = new THREE.PerspectiveCamera( 45, viewerEl.clientWidth / viewerEl.clientHeight, 0.1, 1000 ); camera.position.set(100, 100, 100); // 创建渲染器 renderer = new THREE.WebGLRenderer({ antialias: true }); renderer.setSize(viewerEl.clientWidth, viewerEl.clientHeight); renderer.setPixelRatio(window.devicePixelRatio); renderer.shadowMap.enabled = true; viewerEl.appendChild(renderer.domElement); // 添加灯光 const ambientLight = new THREE.AmbientLight(0xffffff, 0.6); scene.add(ambientLight); const directionalLight = new THREE.DirectionalLight(0xffffff, 0.8); directionalLight.position.set(50, 100, 50); directionalLight.castShadow = true; scene.add(directionalLight); // 添加坐标轴辅助 const axesHelper = new THREE.AxesHelper(10); scene.add(axesHelper); // 鼠标控制 const handleMouseDown = (e: MouseEvent) => { isDragging = true; previousMousePosition = { x: e.clientX, y: e.clientY }; }; const handleMouseMove = (e: MouseEvent) => { if (!isDragging || !group) return; const deltaX = e.clientX - previousMousePosition.x; const deltaY = e.clientY - previousMousePosition.y; group.rotation.y += deltaX * 0.01; group.rotation.x += deltaY * 0.01; previousMousePosition = { x: e.clientX, y: e.clientY }; }; const handleMouseUp = () => { isDragging = false; }; viewerEl.addEventListener("mousedown", handleMouseDown); document.addEventListener("mousemove", handleMouseMove); document.addEventListener("mouseup", handleMouseUp); // 动画循环 const animate = () => { if (scene && camera && renderer && group) { if (!isDragging) { group.rotation.y += 0.005; // 自动旋转 } renderer.render(scene, camera); } animationId = requestAnimationFrame(animate); }; animate(); // 清理函数 onCleanup(() => { if (animationId) cancelAnimationFrame(animationId); viewerEl.removeEventListener("mousedown", handleMouseDown); document.removeEventListener("mousemove", handleMouseMove); document.removeEventListener("mouseup", handleMouseUp); if (renderer) { renderer.dispose(); viewerEl.removeChild(renderer.domElement); } if (scene) scene.clear(); }); }); createEffect(() => { if (props.url) { load3mf(props.url); } }); return (
拖动旋转
⚠️ {error()}
加载模型中...
); }