import React, { Suspense, useState, useEffect } from 'react'; import { Canvas, useLoader } from '@react-three/fiber'; import { OBJLoader } from 'three/examples/jsm/loaders/OBJLoader'; import { MTLLoader } from 'three/examples/jsm/loaders/MTLLoader'; import { OrbitControls, Stage, useProgress, Environment, ContactShadows } from '@react-three/drei'; import { Spin } from 'antd'; import JSZip from 'jszip'; import * as THREE from 'three'; class ErrorBoundary extends React.Component { constructor(props) { super(props); this.state = { hasError: false }; } static getDerivedStateFromError(error) { return { hasError: true }; } componentDidCatch(error, errorInfo) { console.error("3D Model Viewer Error:", error, errorInfo); } render() { if (this.state.hasError) { return (
3D 模型加载失败
); } return this.props.children; } } const Model = ({ objPath, mtlPath, scale = 1 }) => { // If mtlPath is provided, load materials first const materials = mtlPath ? useLoader(MTLLoader, mtlPath) : null; const obj = useLoader(OBJLoader, objPath, (loader) => { if (materials) { materials.preload(); loader.setMaterials(materials); } }); const clone = obj.clone(); return ; }; const LoadingOverlay = () => { const { progress, active } = useProgress(); if (!active) return null; return (
{progress.toFixed(0)}% Loading
); }; const ModelViewer = ({ objPath, mtlPath, scale = 1, autoRotate = true }) => { const [paths, setPaths] = useState(null); const [unzipping, setUnzipping] = useState(false); const [error, setError] = useState(null); useEffect(() => { let isMounted = true; const blobUrls = []; const loadPaths = async () => { if (!objPath) return; // 如果是 zip 文件 if (objPath.toLowerCase().endsWith('.zip')) { setUnzipping(true); setError(null); try { const response = await fetch(objPath); const arrayBuffer = await response.arrayBuffer(); const zip = await JSZip.loadAsync(arrayBuffer); let extractedObj = null; let extractedMtl = null; const fileMap = {}; // 1. 提取所有文件并创建 Blob URL 映射 for (const [filename, file] of Object.entries(zip.files)) { if (file.dir) continue; const content = await file.async('blob'); const url = URL.createObjectURL(content); blobUrls.push(url); // 记录文件名到 URL 的映射,用于后续材质引用图片等情况 const baseName = filename.split('/').pop(); fileMap[baseName] = url; if (filename.toLowerCase().endsWith('.obj')) { extractedObj = url; } else if (filename.toLowerCase().endsWith('.mtl')) { extractedMtl = url; } } if (isMounted) { if (extractedObj) { setPaths({ obj: extractedObj, mtl: extractedMtl }); } else { setError('压缩包内未找到 .obj 模型文件'); } } } catch (err) { console.error('Error unzipping model:', err); if (isMounted) setError('加载压缩包失败'); } finally { if (isMounted) setUnzipping(false); } } else { // 普通路径 setPaths({ obj: objPath, mtl: mtlPath }); } }; loadPaths(); return () => { isMounted = false; // 清理 Blob URL 释放内存 blobUrls.forEach(url => URL.revokeObjectURL(url)); }; }, [objPath, mtlPath]); if (unzipping) { return (
正在解压 3D 资源...
); } if (error) { return (
{error}
); } if (!paths) return null; return (
); }; export default ModelViewer;