refactor: stl->3mf

This commit is contained in:
hyper
2026-03-15 23:17:16 +08:00
parent 7be206581d
commit 2476659fc6
5 changed files with 464 additions and 52 deletions
+48 -31
View File
@@ -5,13 +5,13 @@ import {
Show, createEffect,
} from "solid-js";
import * as THREE from "three";
import { STLLoader } from "three/examples/jsm/loaders/STLLoader.js";
import { ThreeMFLoader } from "three/addons/loaders/3MFLoader.js";
export interface TokenViewerProps {
stlUrl: string | null;
}
export default function MdTokenViewer(props: TokenViewerProps) {
export default function MdTokenViewer(props: TokenViewerProps) {
const [viewerRef, setViewerRef] = createSignal<HTMLDivElement | null>(null);
const [isLoaded, setIsLoaded] = createSignal(false);
@@ -21,49 +21,66 @@ export default function MdTokenViewer(props: TokenViewerProps) {
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 };
// 加载 STL 用于预览
// 加载 3MF 用于预览
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();
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 response = await fetch(url);
const stlText = await response.text();
const loader = new ThreeMFLoader();
const object = await loader.loadAsync(url);
// 解析 STL
const loader = new STLLoader();
const geometry = loader.parse(stlText);
// 3MF 文件可能返回一个 Group,包含多个 Mesh
group = object instanceof THREE.Group ? object : new THREE.Group().add(object);
// 计算边界并居中
geometry.center();
// 居中并调整方向
group.center();
group.rotateX(Math.PI);
// 创建材质
const material = new THREE.MeshStandardMaterial({
color: 0x808080,
metalness: 0.3,
roughness: 0.7,
side: THREE.DoubleSide,
// 为每个 mesh 启用原始颜色
group.traverse((child) => {
if ((child as THREE.Mesh).isMesh) {
const childMesh = child as THREE.Mesh;
// 保留原始顶点颜色或材质颜色
if (childMesh.geometry.attributes.color) {
childMesh.material.vertexColors = true;
}
// 确保材质是标准材质以支持光照
if (!(childMesh.material instanceof THREE.MeshStandardMaterial)) {
childMesh.material = new THREE.MeshStandardMaterial({
color: childMesh.material.color,
metalness: 0.3,
roughness: 0.7,
});
}
}
});
mesh = new THREE.Mesh(geometry, material);
mesh.rotateX(Math.PI);
scene?.add(mesh);
scene?.add(group);
mesh = group.children[0] as THREE.Mesh;
// 调整相机
if (camera && scene) {
const box = new THREE.Box3().setFromObject(mesh);
const box = new THREE.Box3().setFromObject(group);
const size = box.getSize(new THREE.Vector3());
const maxDim = Math.max(size.x, size.y, size.z);
@@ -73,8 +90,8 @@ export default function MdTokenViewer(props: TokenViewerProps) {
setIsLoaded(true);
} catch (e) {
console.error("加载 STL 预览失败:", e);
setError(e instanceof Error ? e.message : "加载 STL 失败");
console.error("加载 3MF 预览失败:", e);
setError(e instanceof Error ? e.message : "加载模型失败");
}
};
@@ -125,13 +142,13 @@ export default function MdTokenViewer(props: TokenViewerProps) {
};
const handleMouseMove = (e: MouseEvent) => {
if (!isDragging || !mesh) return;
if (!isDragging || !group) 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;
group.rotation.y += deltaX * 0.01;
group.rotation.x += deltaY * 0.01;
previousMousePosition = { x: e.clientX, y: e.clientY };
};
@@ -154,9 +171,9 @@ export default function MdTokenViewer(props: TokenViewerProps) {
// 动画循环
const animate = () => {
if (scene && camera && renderer && mesh) {
if (scene && camera && renderer && group) {
if (!isDragging) {
mesh.rotation.y += 0.005; // 自动旋转
group.rotation.y += 0.005; // 自动旋转
}
renderer.render(scene, camera);
}