Files
DianZhanDemo/app/composables/powerStation/modes/useFullMode.ts
ch197511161 ddce8fce18 init5
2025-12-11 01:29:41 +08:00

364 lines
10 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

/**
* 完整模式 - 模型加载
* 支持完整的模型层级结构和合并优化
*/
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<string, number>()
// 更新世界矩阵
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,
}
}