This commit is contained in:
ch197511161
2025-12-11 01:29:41 +08:00
parent 908b4361ed
commit ddce8fce18
473 changed files with 267270 additions and 0 deletions

View File

@@ -0,0 +1,363 @@
/**
* 完整模式 - 模型加载
* 支持完整的模型层级结构和合并优化
*/
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,
}
}

View File

@@ -0,0 +1,219 @@
/**
* 简化模式 - 模型加载
* 按文件名直接加载模型,支持按需加载和自动回退
*/
import { ElMessage } from 'element-plus'
import type * as THREE from 'three'
import { DRACOLoader } from 'three/examples/jsm/loaders/DRACOLoader.js'
import { GLTFLoader } from 'three/examples/jsm/loaders/GLTFLoader.js'
import { markRaw } from 'vue'
import type { ModelModeContext } from '../types'
// ============================================================
// 配置常量
// ============================================================
/** DRACO解码器路径 */
const DRACO_DECODER_PATH = 'https://www.gstatic.com/draco/v1/decoders/'
/** 简化模式配置 */
const SIMPLIFIED_MODE_CONFIG = {
/** 层级显示的最大深度 */
maxHierarchyDepth: 2,
/** 模型文件扩展名 */
fileExtension: '.glb',
} as const
// ============================================================
// 主钩子函数
// ============================================================
/**
* 简化模式组合式函数
* @param context - 模型模式上下文
*/
export function useSimplifiedMode(context: ModelModeContext) {
const {
store,
isLoading,
archetypeModel,
modelGroup,
objectMap,
modelCache,
replaceWithPBRMaterial,
generateHierarchy,
initEnvironment,
applyLightingMode,
fitView,
rebuildObjectMap,
generateBreadcrumbs,
} = context
// ============================================================
// 模型设置
// ============================================================
/**
* 从缓存设置模型
* @param model - 缓存的模型
* @param fileName - 文件名
*/
const setupModelFromCache = (model: THREE.Object3D, fileName: string) => {
const clonedModel = model.clone()
const nameWithoutExt = fileName.replace(SIMPLIFIED_MODE_CONFIG.fileExtension, '')
// 设置模型名称
clonedModel.name = nameWithoutExt
// 更新原型模型
archetypeModel.value = markRaw(clonedModel.clone())
// 清空并添加到模型组
modelGroup.clear()
modelGroup.add(clonedModel)
// 生成层级结构(只显示指定深度的节点)
let hierarchy = []
if (clonedModel.children.length > 0) {
// 如果模型有子节点,将每个子节点作为一级节点
clonedModel.children.forEach((child, index) => {
const childPath = index === 0 ? nameWithoutExt : `${nameWithoutExt}~child${index}`
const childHierarchy = generateHierarchy(
child,
childPath,
1,
SIMPLIFIED_MODE_CONFIG.maxHierarchyDepth
)
hierarchy.push(childHierarchy)
})
} else {
// 如果模型没有子节点,使用模型本身作为一级节点
const modelHierarchy = generateHierarchy(
clonedModel,
nameWithoutExt,
1,
SIMPLIFIED_MODE_CONFIG.maxHierarchyDepth
)
hierarchy = [modelHierarchy]
}
store.modelHierarchy = hierarchy
// 生成面包屑(返回供调用者处理)
const breadcrumbs = generateBreadcrumbs(nameWithoutExt)
// 重建物体映射
objectMap.clear()
rebuildObjectMap(clonedModel)
// 初始化环境和光照
initEnvironment()
applyLightingMode()
// 适配视图
fitView(clonedModel)
}
// ============================================================
// 模型加载
// ============================================================
/**
* 根据命名约定构建模型URL
* @param fileName - 文件名
* @returns 完整URL路径
*/
const buildModelUrl = (fileName: string): string => {
// 格式: Root~Parent~Child.glb -> /3DModels/Root/Root~Parent~Child.glb
const nameWithoutExt = fileName.slice(0, -4)
const rootFolder = nameWithoutExt.split('~')[0]
return `/3DModels/${rootFolder}/${fileName}`
}
/**
* 按文件名加载模型
* @param fileName - 文件名(可带或不带.glb扩展名
* @param onLoaded - 加载完成回调
*/
const loadModelByFileName = (fileName: string, onLoaded?: () => void) => {
// 确保有.glb扩展名
if (!fileName.endsWith(SIMPLIFIED_MODE_CONFIG.fileExtension)) {
fileName += SIMPLIFIED_MODE_CONFIG.fileExtension
}
// 检查缓存
if (store.modelCacheEnabled && modelCache.has(fileName)) {
const cached = modelCache.get(fileName)!
setupModelFromCache(cached, fileName)
if (onLoaded) onLoaded()
return
}
// 开始加载
isLoading.value = true
store.isModelLoading = true
const url = buildModelUrl(fileName)
console.log(`${url} 加载模型`)
// 创建加载器
const loader = new GLTFLoader()
const dracoLoader = new DRACOLoader()
dracoLoader.setDecoderPath(DRACO_DECODER_PATH)
loader.setDRACOLoader(dracoLoader)
loader.load(
url,
gltf => {
const rawModel = gltf.scene
replaceWithPBRMaterial(rawModel)
// 缓存模型
if (store.modelCacheEnabled) {
modelCache.set(fileName, rawModel.clone())
}
// 设置模型
setupModelFromCache(rawModel, fileName)
isLoading.value = false
store.isModelLoading = false
dracoLoader.dispose()
if (onLoaded) onLoaded()
},
undefined,
error => {
console.warn('加载模型失败:', fileName, error)
ElMessage.error(`模型加载失败: ${fileName},请检查网络连接或模型文件是否存在`)
// 回退逻辑:尝试加载父级模型
const nameWithoutExt = fileName.slice(0, -4)
const parts = nameWithoutExt.split('~')
if (parts.length > 1) {
// 移除最后一级,尝试加载父级
parts.pop()
const parentName = parts.join('~')
console.log('回退到父级:', parentName)
loadModelByFileName(parentName, onLoaded)
} else {
// 没有父级可回退
console.error('没有父级可以回退,或根节点加载失败。')
isLoading.value = false
store.isModelLoading = false
dracoLoader.dispose()
}
}
)
}
// ============================================================
// 导出接口
// ============================================================
return {
/** 按文件名加载模型 */
loadModelByFileName,
}
}