/** * 模型管理系统 * 处理3D模型的加载、层级结构生成、材质处理和相机适配 */ import * as TWEEN from '@tweenjs/tween.js' import * as THREE from 'three' import { RoomEnvironment } from 'three/examples/jsm/environments/RoomEnvironment.js' import { usePowerStationStore } from '~/stores/powerStation' import { useFullMode } from './modes/useFullMode' import { useSimplifiedMode } from './modes/useSimplifiedMode' import type { BreadcrumbItem, LightingMode, ModelHierarchyNode } from './types' // ============================================================ // 配置常量 // ============================================================ /** 光源配置 */ const LIGHT_CONFIG = { /** 环境光颜色 */ ambientColor: 0xffffff, /** 环境光强度 */ ambientIntensity: 0.6, /** 方向光颜色 */ directionalColor: 0xffffff, /** 方向光强度 */ directionalIntensity: 1, /** 方向光位置 */ directionalPosition: new THREE.Vector3(5, 3, 5), } as const /** PBR材质配置 */ const PBR_MATERIAL_CONFIG = { /** 金属度 */ metalness: 0.6, /** 粗糙度 */ roughness: 0.6, /** 环境贴图强度 */ envMapIntensity: 1.5, } as const /** 相机动画配置 */ const CAMERA_ANIMATION_CONFIG = { /** 动画时长(毫秒) */ duration: 900, /** 距离系数 */ distanceMultiplier: 1.2, } as const // ============================================================ // 主钩子函数 // ============================================================ /** * 模型管理组合式函数 * @param getScene - 获取场景 * @param getCamera - 获取相机 * @param getControls - 获取控制器 * @param getRenderer - 获取渲染器 */ export function useModelManager( getScene: () => THREE.Scene, getCamera: () => THREE.PerspectiveCamera, getControls: () => any, // OrbitControls getRenderer: () => THREE.WebGLRenderer ) { const store = usePowerStationStore() const isLoading = ref(true) // ============================================================ // 模型数据 // ============================================================ // 使用 markRaw 标记 Three.js 对象为非响应式,防止 Vue DevTools 追踪导致性能问题 const modelGroup = markRaw(new THREE.Group()) const archetypeModel = shallowRef(null) // 使用普通 Map(Three.js 对象已是 markRaw,Map 本身不需要响应式) const objectMap = new Map() const modelCache = new Map() // 环境贴图 let envTexture: THREE.Texture | null = null // 光照模式 const lightingMode = ref('basic') // ============================================================ // 初始化 // ============================================================ /** * 初始化模型系统 * 添加模型组和基础光源到场景 */ const initModelSystem = () => { const scene = getScene() if (!scene) return // 添加模型组 scene.add(modelGroup) // 添加基础光源 const ambientLight = new THREE.AmbientLight( LIGHT_CONFIG.ambientColor, LIGHT_CONFIG.ambientIntensity ) scene.add(ambientLight) const dirLight = new THREE.DirectionalLight( LIGHT_CONFIG.directionalColor, LIGHT_CONFIG.directionalIntensity ) dirLight.position.copy(LIGHT_CONFIG.directionalPosition) scene.add(dirLight) } /** * 初始化环境贴图 */ const initEnvironment = () => { const renderer = getRenderer() const scene = getScene() if (!renderer || !scene) return // 避免重复创建 if (envTexture) return const pmremGenerator = new THREE.PMREMGenerator(renderer) pmremGenerator.compileEquirectangularShader() const roomEnvironment = new RoomEnvironment() envTexture = pmremGenerator.fromScene(roomEnvironment).texture if (lightingMode.value === 'advanced') { scene.environment = envTexture } // 清理临时对象 roomEnvironment.dispose() pmremGenerator.dispose() } // ============================================================ // 层级结构生成 // ============================================================ /** * 生成模型层级结构 * @param object - Three.js 对象 * @param currentPath - 当前路径 * @param depth - 当前深度 * @param maxDepth - 最大深度 * @returns 层级节点 */ const generateHierarchy = ( object: THREE.Object3D, currentPath: string, depth: number = 1, maxDepth: number = Infinity ): ModelHierarchyNode => { const isRoot = object === archetypeModel.value const nodeId = isRoot ? store.currentNodeId || currentPath : currentPath // 设置 userData.id object.userData.id = nodeId const node: ModelHierarchyNode = { id: nodeId, label: isRoot ? store.currentNodeId || object.name || `节点 ${nodeId}` : object.name || `节点 ${nodeId}`, children: [], } // 检查是否为完整物体(有名称且包含多个子Mesh) const isCompleteObject = object.name && object.children.filter(child => child.type === 'Mesh').length > 1 // 达到最大深度则停止递归 if (depth >= maxDepth) { return node } // 递归处理子节点 object.children.forEach(child => { if (child.type === 'Mesh' || child.type === 'Group' || child.type === 'Object3D') { const childName = child.name || '子对象' const childPath = `${currentPath}~${childName}` // 跳过完整物体中的无名Mesh子节点 if (isCompleteObject && child.type === 'Mesh' && !child.name) { return } const childNode = generateHierarchy(child, childPath, depth + 1, maxDepth) node.children.push(childNode) } }) return node } // ============================================================ // 节点查找 // ============================================================ /** * 通过ID查找节点 * @param root - 根对象 * @param id - 节点ID * @returns 找到的节点或null */ const findNodeById = (root: THREE.Object3D, id: string): THREE.Object3D | null => { if (root.userData.id === id) return root for (const child of root.children) { const found = findNodeById(child, id) if (found) return found } return null } /** * 重建物体映射表 * @param root - 根对象 */ const rebuildObjectMap = (root: THREE.Object3D) => { if (root.userData.id) { objectMap.set(root.userData.id, root) } root.children.forEach(child => rebuildObjectMap(child)) } // ============================================================ // 面包屑生成 // ============================================================ /** * 生成面包屑导航数据 * @param nodeId - 节点ID * @returns 面包屑数组 */ const generateBreadcrumbs = (nodeId: string): BreadcrumbItem[] => { if (!archetypeModel.value) return [] const breadcrumbsList: BreadcrumbItem[] = [] const nodeIdParts = nodeId.split('~') if (nodeIdParts.length > 0) { let currentPath = '' for (let i = 0; i < nodeIdParts.length; i++) { const part = nodeIdParts[i] currentPath = i === 0 ? part : `${currentPath}~${part}` const currentObject = findNodeById(archetypeModel.value, currentPath) const displayText = currentObject ? currentObject.name : part || findNodeById(archetypeModel.value, currentPath)?.name || part const item: BreadcrumbItem = { text: displayText } if (currentPath !== nodeId) { item.to = `?currentNodeId=${currentPath}` } breadcrumbsList.push(item) } } // 使用当前节点ID的第一部分作为面包屑首项 if (breadcrumbsList.length > 0 && store.currentNodeId) { const firstPart = store.currentNodeId.split('~')[0] breadcrumbsList[0].text = firstPart } return breadcrumbsList } // ============================================================ // 相机适配 // ============================================================ /** * 适配视图到目标对象 * @param targetObj - 目标对象(默认为modelGroup) * @param keepOrientation - 是否保持当前相机朝向 */ const fitView = (targetObj?: THREE.Object3D, keepOrientation: boolean = true) => { const obj = targetObj || modelGroup const camera = getCamera() const controls = getControls() console.log('[fitView] 开始执行:', { hasTargetObj: !!targetObj, objName: obj?.name, objChildrenCount: obj?.children?.length, hasCamera: !!camera, hasControls: !!controls, }) if (!obj || !camera || !controls) { console.warn('[fitView] 提前返回: obj/camera/controls 缺失') return } // 计算包围盒 const box = new THREE.Box3().setFromObject(obj) console.log('[fitView] 包围盒:', { isEmpty: box.isEmpty(), min: box.min.toArray(), max: box.max.toArray(), }) if (box.isEmpty()) { console.warn('[fitView] 提前返回: 包围盒为空') return } const size = box.getSize(new THREE.Vector3()) const center = box.getCenter(new THREE.Vector3()) // 计算适配距离 const maxSize = Math.max(size.x, size.y, size.z) const fitHeightDistance = maxSize / (2 * Math.atan((Math.PI * camera.fov) / 360)) const fitWidthDistance = fitHeightDistance / camera.aspect const distance = CAMERA_ANIMATION_CONFIG.distanceMultiplier * Math.max(fitHeightDistance, fitWidthDistance) // 计算相机方向 let direction: THREE.Vector3 if (keepOrientation) { direction = new THREE.Vector3().subVectors(camera.position, controls.target).normalize() if (direction.lengthSq() < 0.0001) { direction = new THREE.Vector3(1, 1, 1).normalize() } } else { direction = new THREE.Vector3(1, 1, 1).normalize() } const targetPosition = center.clone().add(direction.multiplyScalar(distance)) console.log('[fitView] 动画参数:', { size: size.toArray(), center: center.toArray(), distance, currentCameraPos: camera.position.toArray(), targetCameraPos: targetPosition.toArray(), }) // 相机位置动画 new TWEEN.Tween(camera.position) .to({ x: targetPosition.x, y: targetPosition.y, z: targetPosition.z }, CAMERA_ANIMATION_CONFIG.duration) .easing(TWEEN.Easing.Quadratic.InOut) .onStart(() => console.log('[fitView] 相机位置动画开始')) .onComplete(() => console.log('[fitView] 相机位置动画完成')) .start() // 控制目标动画 new TWEEN.Tween(controls.target) .to({ x: center.x, y: center.y, z: center.z }, CAMERA_ANIMATION_CONFIG.duration) .easing(TWEEN.Easing.Quadratic.InOut) .onStart(() => console.log('[fitView] 控制目标动画开始')) .onUpdate(() => { controls.update() }) .onComplete(() => console.log('[fitView] 控制目标动画完成')) .start() } // ============================================================ // 材质处理 // ============================================================ /** * 替换为PBR材质 * @param object - 要处理的对象 */ const replaceWithPBRMaterial = (object: THREE.Object3D) => { object.traverse(child => { if ((child as THREE.Mesh).isMesh) { const mesh = child as THREE.Mesh const materials = Array.isArray(mesh.material) ? mesh.material : [mesh.material] const newMaterials: THREE.MeshStandardMaterial[] = [] materials.forEach(mat => { const color = (mat as THREE.MeshBasicMaterial).color instanceof THREE.Color ? (mat as THREE.MeshBasicMaterial).color.clone() : new THREE.Color(0xaaaaaa) // 获取原始贴图 const originalMap = (mat as THREE.MeshBasicMaterial).map || null const alphaMap = originalMap || null const useAlpha = originalMap !== null const pbrMaterial = new THREE.MeshStandardMaterial({ color: color, metalness: PBR_MATERIAL_CONFIG.metalness, roughness: PBR_MATERIAL_CONFIG.roughness, transparent: useAlpha || ((mat as THREE.MeshBasicMaterial).transparent ?? false), opacity: useAlpha ? 1 : ((mat as THREE.MeshBasicMaterial).opacity ?? 1), side: mat.side || THREE.FrontSide, map: originalMap, alphaMap: alphaMap, normalMap: (mat as THREE.MeshStandardMaterial).normalMap || null, envMapIntensity: PBR_MATERIAL_CONFIG.envMapIntensity, }) newMaterials.push(pbrMaterial) }) if (newMaterials.length > 0) { mesh.material = (Array.isArray(mesh.material) ? newMaterials : newMaterials[0]) as | THREE.Material | THREE.Material[] } } }) } /** * 更新材质模式 * @param root - 根对象 * @param mode - 光照模式 */ const updateMaterials = (root: THREE.Object3D, mode: LightingMode) => { root.traverse(child => { if ((child as THREE.Mesh).isMesh) { const mesh = child as THREE.Mesh const materials = Array.isArray(mesh.material) ? mesh.material : [mesh.material] const newMaterials = materials.map(mat => { if (mode === 'basic') { // 切换到基础模式 if (mat.type === 'MeshLambertMaterial') return mat if (mat.userData.lambertPartner) return mat.userData.lambertPartner const source = mat as THREE.MeshStandardMaterial const lambert = new THREE.MeshLambertMaterial({ color: source.color, map: source.map, transparent: source.transparent, opacity: source.opacity, side: source.side, alphaMap: source.alphaMap, aoMap: source.aoMap, }) mat.userData.lambertPartner = lambert lambert.userData.pbrPartner = mat return lambert } else { // 切换到高级模式 if (mat.type === 'MeshStandardMaterial') return mat if (mat.userData.pbrPartner) return mat.userData.pbrPartner return mat } }) mesh.material = Array.isArray(mesh.material) ? newMaterials : newMaterials[0] } }) } // ============================================================ // 光照模式控制 // ============================================================ /** * 切换光照模式 */ const toggleLighting = () => { lightingMode.value = lightingMode.value === 'basic' ? 'advanced' : 'basic' applyLightingMode() } /** * 应用当前光照模式 */ const applyLightingMode = () => { const scene = getScene() if (!scene) return if (lightingMode.value === 'advanced') { if (envTexture) scene.environment = envTexture } else { scene.environment = null } if (modelGroup) updateMaterials(modelGroup, lightingMode.value) if (archetypeModel.value) updateMaterials(archetypeModel.value, lightingMode.value) } // ============================================================ // 模式上下文 // ============================================================ /** 供加载模式使用的共享上下文 */ const context = { store, isLoading, archetypeModel, modelGroup, objectMap, modelCache, replaceWithPBRMaterial, generateHierarchy, initEnvironment, applyLightingMode, fitView, findNodeById, rebuildObjectMap, generateBreadcrumbs, } // 实例化加载模式 const { loadModel, processModel } = useFullMode(context) const { loadModelByFileName } = useSimplifiedMode(context) // ============================================================ // 导出接口 // ============================================================ return { // 数据 modelGroup, isLoading, lightingMode, objectMap, archetypeModel, // 初始化 initModelSystem, // 模型加载 loadModel, processModel, loadModelByFileName, // 视图控制 fitView, // 光照控制 toggleLighting, // 工具函数 findNodeById, generateBreadcrumbs, } }