From e92065d14c301f244523b00b07cdf69108539eeb Mon Sep 17 00:00:00 2001 From: hypercross Date: Sun, 15 Mar 2026 19:14:39 +0800 Subject: [PATCH] refactor: token viewer --- src/components/index.ts | 1 + src/components/md-token-viewer.tsx | 211 +++++++++++++++++++++++++++++ src/components/md-token.tsx | 176 +----------------------- 3 files changed, 213 insertions(+), 175 deletions(-) create mode 100644 src/components/md-token-viewer.tsx diff --git a/src/components/index.ts b/src/components/index.ts index ebf8434..c234516 100644 --- a/src/components/index.ts +++ b/src/components/index.ts @@ -8,6 +8,7 @@ import './md-deck'; import './md-commander/index'; import './md-yarn-spinner'; import './md-token'; +import './md-token-viewer'; // 导出组件 export { Article } from './Article'; diff --git a/src/components/md-token-viewer.tsx b/src/components/md-token-viewer.tsx new file mode 100644 index 0000000..2b16ac7 --- /dev/null +++ b/src/components/md-token-viewer.tsx @@ -0,0 +1,211 @@ +import { customElement, noShadowDOM } from "solid-element"; +import { + createSignal, + onMount, + onCleanup, + Show, +} from "solid-js"; +import * as THREE from "three"; +import { STLLoader } from "three/examples/jsm/loaders/STLLoader.js"; + +export interface TokenViewerProps { + stlUrl: string; +} + +customElement("md-token-viewer", {}, (props, { element }) => { + noShadowDOM(); + + 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 animationId: number | null = null; + let isDragging = false; + let previousMousePosition = { x: 0, y: 0 }; + + // 加载 STL 用于预览 + const loadSTL = async (url: string) => { + const viewerEl = viewerRef(); + if (!viewerEl) return; + + // 清理旧的场景 + if (mesh) { + scene?.remove(mesh); + mesh.geometry.dispose(); + (mesh.material as THREE.Material).dispose(); + mesh = null; + } + + try { + const response = await fetch(url); + const stlText = await response.text(); + + // 解析 STL + const loader = new STLLoader(); + const geometry = loader.parse(stlText); + + // 计算边界并居中 + geometry.center(); + + // 创建材质 + const material = new THREE.MeshStandardMaterial({ + color: 0x808080, + metalness: 0.3, + roughness: 0.7, + side: THREE.DoubleSide, + }); + + mesh = new THREE.Mesh(geometry, material); + scene?.add(mesh); + + // 调整相机 + if (camera && scene) { + const box = new THREE.Box3().setFromObject(mesh); + 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("加载 STL 预览失败:", e); + setError(e instanceof Error ? e.message : "加载 STL 失败"); + } + }; + + // 初始化 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 || !mesh) return; + + const deltaX = e.clientX - previousMousePosition.x; + const deltaY = e.clientY - previousMousePosition.y; + + mesh.rotation.y += deltaX * 0.01; + mesh.rotation.x += deltaY * 0.01; + + previousMousePosition = { x: e.clientX, y: e.clientY }; + }; + + const handleMouseUp = () => { + isDragging = false; + }; + + const handleWheel = (e: WheelEvent) => { + if (!camera) return; + + const zoomSpeed = 0.5; + camera.position.multiplyScalar(e.deltaY > 0 ? 1 + zoomSpeed : 1 - zoomSpeed); + }; + + viewerEl.addEventListener("mousedown", handleMouseDown); + document.addEventListener("mousemove", handleMouseMove); + document.addEventListener("mouseup", handleMouseUp); + viewerEl.addEventListener("wheel", handleWheel); + + // 动画循环 + const animate = () => { + if (scene && camera && renderer && mesh) { + if (!isDragging) { + mesh.rotation.y += 0.005; // 自动旋转 + } + renderer.render(scene, camera); + } + animationId = requestAnimationFrame(animate); + }; + animate(); + + // 加载 STL 模型 + const stlUrl = element.getAttribute("stl-url"); + if (stlUrl) { + loadSTL(stlUrl); + } + + // 清理函数 + onCleanup(() => { + if (animationId) cancelAnimationFrame(animationId); + viewerEl.removeEventListener("mousedown", handleMouseDown); + document.removeEventListener("mousemove", handleMouseMove); + document.removeEventListener("mouseup", handleMouseUp); + viewerEl.removeEventListener("wheel", handleWheel); + + if (renderer) { + renderer.dispose(); + viewerEl.removeChild(renderer.domElement); + } + if (scene) scene.clear(); + }); + }); + + return ( +
+
+ 拖动旋转 | 滚动缩放 +
+ +
+ ⚠️ {error()} +
+
+ +
+
+ 加载模型中... +
+ +
+ ); +}); diff --git a/src/components/md-token.tsx b/src/components/md-token.tsx index de70f15..d06fef5 100644 --- a/src/components/md-token.tsx +++ b/src/components/md-token.tsx @@ -5,15 +5,11 @@ import { createResource, createMemo, createSignal, - onMount, onCleanup, - createEffect, } from "solid-js"; import { resolvePath } from "./utils/path"; import { traceImage, type TracedLayer } from "./utils/image-tracer"; import { generateSTL, type ExtrusionSettings } from "./utils/stl-generator"; -import * as THREE from "three"; -import { STLLoader } from "three/examples/jsm/loaders/STLLoader.js"; export interface TokenProps { size?: number; // 模型整体尺寸 (mm), 默认 50 @@ -36,15 +32,6 @@ customElement("md-token", { size: 50, defaultThickness: 2 }, (props, { element } const [stlUrl, setStlUrl] = createSignal(null); const [isGenerating, setIsGenerating] = createSignal(false); const [error, setError] = createSignal(null); - const [viewerRef, setViewerRef] = 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 animationId: number | null = null; - let isDragging = false; - let previousMousePosition = { x: 0, y: 0 }; // 从 element 的 textContent 获取图片路径 const rawSrc = element?.textContent?.trim() || ""; @@ -129,9 +116,6 @@ customElement("md-token", { size: 50, defaultThickness: 2 }, (props, { element } const stlBlob = await generateSTL(image()!, traceResult()!, settings); const url = URL.createObjectURL(stlBlob); setStlUrl(url); - - // 更新 3D 预览 - loadSTLForPreview(url); } catch (e) { setError(e instanceof Error ? e.message : "生成模型失败"); } finally { @@ -139,156 +123,6 @@ customElement("md-token", { size: 50, defaultThickness: 2 }, (props, { element } } }; - // 加载 STL 用于预览 - const loadSTLForPreview = async (url: string) => { - const viewerEl = viewerRef(); - if (!viewerEl) return; - - // 清理旧的场景 - if (mesh) { - scene?.remove(mesh); - mesh.geometry.dispose(); - (mesh.material as THREE.Material).dispose(); - mesh = null; - } - - try { - const response = await fetch(url); - const stlText = await response.text(); - - // 解析 STL - const loader = new STLLoader(); - const geometry = loader.parse(stlText); - - // 计算边界并居中 - geometry.center(); - - // 创建材质 - const material = new THREE.MeshStandardMaterial({ - color: 0x808080, - metalness: 0.3, - roughness: 0.7, - side: THREE.DoubleSide, - }); - - mesh = new THREE.Mesh(geometry, material); - scene?.add(mesh); - - // 调整相机 - if (camera && scene) { - const box = new THREE.Box3().setFromObject(mesh); - 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); - } - } catch (e) { - console.error("加载 STL 预览失败:", e); - } - }; - - // 初始化 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 || !mesh) return; - - const deltaX = e.clientX - previousMousePosition.x; - const deltaY = e.clientY - previousMousePosition.y; - - mesh.rotation.y += deltaX * 0.01; - mesh.rotation.x += deltaY * 0.01; - - previousMousePosition = { x: e.clientX, y: e.clientY }; - }; - - const handleMouseUp = () => { - isDragging = false; - }; - - const handleWheel = (e: WheelEvent) => { - if (!camera) return; - - const zoomSpeed = 0.5; - camera.position.multiplyScalar(e.deltaY > 0 ? 1 + zoomSpeed : 1 - zoomSpeed); - }; - - viewerEl.addEventListener("mousedown", handleMouseDown); - document.addEventListener("mousemove", handleMouseMove); - document.addEventListener("mouseup", handleMouseUp); - viewerEl.addEventListener("wheel", handleWheel); - - // 动画循环 - const animate = () => { - if (scene && camera && renderer && mesh) { - if (!isDragging) { - mesh.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); - viewerEl.removeEventListener("wheel", handleWheel); - - if (renderer) { - renderer.dispose(); - viewerEl.removeChild(renderer.domElement); - } - if (scene) scene.clear(); - }); - }); - // 更新图层厚度 const updateLayerThickness = (layerId: string, thickness: number) => { setLayers((prev) => @@ -348,15 +182,7 @@ customElement("md-token", { size: 50, defaultThickness: 2 }, (props, { element }

3D 模型预览

-
-
- 拖动旋转 | 滚动缩放 -
-
+