Files
DianZhanDemo/app/composables/powerStation/modes/useFullMode.ts

364 lines
10 KiB
TypeScript
Raw Normal View History

2025-12-11 01:29:41 +08:00
/**
* -
*
*/
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,
}
}