/** * 完整模式 - 模型加载 * 支持完整的模型层级结构和合并优化 */ import { ElMessage } from 'element-plus' import * as THREE from 'three' import { DRACOLoader } from 'three/examples/jsm/loaders/DRACOLoader.js' import { GLTFLoader } from 'three/examples/jsm/loaders/GLTFLoader.js' import * as BufferGeometryUtils from 'three/examples/jsm/utils/BufferGeometryUtils.js' import { markRaw } from 'vue' import type { ModelModeContext } from '../types' // ============================================================ // 配置常量 // ============================================================ /** DRACO解码器路径 */ const DRACO_DECODER_PATH = 'https://www.gstatic.com/draco/v1/decoders/' // ============================================================ // 辅助函数 // ============================================================ /** * 从URL提取文件名(不含路径和扩展名) * @param url - 文件URL * @returns 文件名 */ const extractFileName = (url: string): string => { const urlParts = url.split('/') const fileNameWithExt = urlParts[urlParts.length - 1] const fileName = fileNameWithExt.split('.').slice(0, -1).join('.') return fileName || '模型' } /** * 递归打印模型中所有物体的路径(用于调试) * @param object - Three.js 对象 * @param path - 当前路径 */ const printModelObjectPaths = (object: THREE.Object3D, path: string = '') => { const fullPath = path ? `${path}/${object.name}` : object.name object.children.forEach(child => { printModelObjectPaths(child, fullPath) }) } // ============================================================ // 几何体合并 // ============================================================ /** * 合并子树中的所有网格 * 用于优化渲染性能 * @param root - 要合并的根对象 */ const mergeSubTree = (root: THREE.Object3D) => { // 收集所有网格 const meshes: THREE.Mesh[] = [] root.traverse(child => { if ((child as THREE.Mesh).isMesh) { meshes.push(child as THREE.Mesh) } }) // 检查是否需要合并 if (meshes.length === 0) return if (meshes.length === 1 && root === meshes[0] && root.children.length === 0) return const geometries: THREE.BufferGeometry[] = [] const materials: THREE.Material[] = [] const materialIndexMap = new Map() // 更新世界矩阵 root.updateMatrixWorld(true) const rootInverse = root.matrixWorld.clone().invert() // 检查UV属性兼容性 const hasUVAttribute = meshes.some(mesh => mesh.geometry.attributes.uv) const allHaveUV = meshes.every(mesh => mesh.geometry.attributes.uv) if (hasUVAttribute && !allHaveUV) { console.warn(`节点 ${root.name} 的几何体UV属性不一致,将移除UV属性以确保合并成功`) } // 处理每个网格 meshes.forEach(mesh => { const geom = mesh.geometry.clone() mesh.updateMatrixWorld(true) const matrix = mesh.matrixWorld.clone().premultiply(rootInverse) geom.applyMatrix4(matrix) // 重新计算法线 if (geom.attributes.normal) { delete geom.attributes.normal } geom.computeVertexNormals() // 处理UV属性兼容性 if (hasUVAttribute && !allHaveUV && geom.attributes.uv) { delete geom.attributes.uv } const meshMaterials = Array.isArray(mesh.material) ? mesh.material : [mesh.material] // 处理几何体组 if (!geom.groups || geom.groups.length === 0) { geom.clearGroups() if (geom.attributes.position) { geom.addGroup(0, geom.attributes.position.count, 0) } } // 重新映射材质索引 const newGroups = [] for (const group of geom.groups) { const originalMat = meshMaterials[group.materialIndex || 0] if (!originalMat) continue const matUuid = originalMat.uuid let newMatIndex = materialIndexMap.get(matUuid) if (newMatIndex === undefined) { newMatIndex = materials.length materials.push(originalMat) materialIndexMap.set(matUuid, newMatIndex) } newGroups.push({ start: group.start, count: group.count, materialIndex: newMatIndex, }) } geom.groups = newGroups geometries.push(geom) }) // 执行合并 if (geometries.length > 0) { try { const mergedGeometry = BufferGeometryUtils.mergeGeometries(geometries, true) // 重新计算法线 if (mergedGeometry.attributes.normal) { mergedGeometry.computeVertexNormals() } const mergedMesh = new THREE.Mesh(mergedGeometry, materials) mergedMesh.name = root.name mergedMesh.userData = { ...root.userData } // 替换原对象 const parent = root.parent if (parent) { mergedMesh.position.copy(root.position) mergedMesh.rotation.copy(root.rotation) mergedMesh.scale.copy(root.scale) parent.remove(root) parent.add(mergedMesh) } } catch (e) { console.warn('合并节点失败:', root.name, e) } } } // ============================================================ // 主钩子函数 // ============================================================ /** * 完整模式组合式函数 * @param context - 模型模式上下文 */ export function useFullMode(context: ModelModeContext) { const { store, isLoading, archetypeModel, modelGroup, objectMap, modelCache, replaceWithPBRMaterial, generateHierarchy, initEnvironment, applyLightingMode, fitView, findNodeById, rebuildObjectMap, generateBreadcrumbs, } = context // ============================================================ // 模型加载 // ============================================================ /** * 加载模型 * @param url - 模型URL * @param onLoaded - 加载完成回调 */ const loadModel = (url: string, onLoaded?: () => void) => { isLoading.value = true store.isModelLoading = true const fileName = extractFileName(url) // 检查缓存 if (store.modelCacheEnabled && modelCache.has(fileName)) { console.log('从缓存加载模型:', fileName) const cachedModel = modelCache.get(fileName) // 使用缓存的模型 const clonedModel = cachedModel!.clone() replaceWithPBRMaterial(clonedModel) archetypeModel.value = markRaw(clonedModel) // 生成层级结构 const rootPath = store.currentNodeId const hierarchy = generateHierarchy(archetypeModel.value, rootPath) store.modelHierarchy = [hierarchy] initEnvironment() applyLightingMode() isLoading.value = false store.isModelLoading = false if (onLoaded) onLoaded() return } // 创建加载器 const loader = new GLTFLoader() const dracoLoader = new DRACOLoader() dracoLoader.setDecoderPath(DRACO_DECODER_PATH) loader.setDRACOLoader(dracoLoader) loader.load( url, gltf => { replaceWithPBRMaterial(gltf.scene) // 缓存模型 if (store.modelCacheEnabled) { modelCache.set(fileName, gltf.scene.clone()) console.log('模型已缓存:', fileName) } // 设置原型模型 archetypeModel.value = markRaw(gltf.scene) // 生成层级结构 const rootPath = store.currentNodeId const hierarchy = generateHierarchy(archetypeModel.value, rootPath) store.modelHierarchy = [hierarchy] initEnvironment() applyLightingMode() isLoading.value = false store.isModelLoading = false dracoLoader.dispose() if (onLoaded) onLoaded() }, undefined, error => { console.error('模型加载错误:', error) ElMessage.error('模型加载失败,请检查网络连接或模型文件是否存在') isLoading.value = false store.isModelLoading = false dracoLoader.dispose() } ) } // ============================================================ // 模型处理 // ============================================================ /** * 处理模型(核心逻辑) * 合并子节点并更新场景 * @param nodeId - 节点ID * @param shouldFitView - 是否适配视图 */ const processModel = (nodeId: string, shouldFitView: boolean = false) => { if (!archetypeModel.value) return // 克隆模型 const newModel = archetypeModel.value.clone() newModel.name = store.currentNodeId || '模型' // 查找目标节点 let targetNode = findNodeById(newModel, nodeId) if (!targetNode) { console.warn(`节点 ${nodeId} 未找到,回退到根节点`) targetNode = newModel } // 合并子节点 const childrenToMerge = [...targetNode.children] childrenToMerge.forEach(child => { mergeSubTree(child) }) // 清空模型组 modelGroup.clear() // 计算世界变换 newModel.updateMatrixWorld(true) const worldPosition = new THREE.Vector3() const worldQuaternion = new THREE.Quaternion() const worldScale = new THREE.Vector3() targetNode.getWorldPosition(worldPosition) targetNode.getWorldQuaternion(worldQuaternion) targetNode.getWorldScale(worldScale) // 添加到模型组 modelGroup.add(targetNode) // 应用世界变换 targetNode.position.copy(worldPosition) targetNode.quaternion.copy(worldQuaternion) targetNode.scale.copy(worldScale) // 重建物体映射 objectMap.clear() rebuildObjectMap(targetNode) // 更新层级结构 const newHierarchy = generateHierarchy(targetNode, targetNode.userData.id) store.modelHierarchy = [newHierarchy] // 生成面包屑(返回供调用者处理) const breadcrumbs = generateBreadcrumbs(nodeId) // 适配视图 if (shouldFitView) { fitView(targetNode) } // 调试:打印物体路径 printModelObjectPaths(newModel) } // ============================================================ // 导出接口 // ============================================================ return { /** 加载状态(只读) */ isLoading: readonly(isLoading), /** 加载模型 */ loadModel, /** 处理模型 */ processModel, } }