Files
DianZhanDemo/app/composables/powerStation/useModelManager.ts

552 lines
17 KiB
TypeScript
Raw Normal View History

2025-12-11 01:29:41 +08:00
/**
*
* 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)
// 使用普通 MapThree.js 对象已是 markRawMap 本身不需要响应式)
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,
}
}