init5
This commit is contained in:
551
app/composables/powerStation/useModelManager.ts
Normal file
551
app/composables/powerStation/useModelManager.ts
Normal file
@@ -0,0 +1,551 @@
|
||||
/**
|
||||
* 模型管理系统
|
||||
* 处理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<THREE.Object3D | null>(null)
|
||||
|
||||
// 使用普通 Map(Three.js 对象已是 markRaw,Map 本身不需要响应式)
|
||||
const objectMap = new Map<string, THREE.Object3D>()
|
||||
const modelCache = new Map<string, THREE.Object3D>()
|
||||
|
||||
// 环境贴图
|
||||
let envTexture: THREE.Texture | null = null
|
||||
|
||||
// 光照模式
|
||||
const lightingMode = ref<LightingMode>('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,
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user