Files
DianZhanDemo/app/composables/powerStation/useInteraction.ts
ch197511161 ddce8fce18 init5
2025-12-11 01:29:41 +08:00

409 lines
12 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

/**
* 三维场景交互系统
* 处理鼠标悬停、点击、双击等交互事件
* 提供物体高亮和轮廓效果
*/
import { navigateTo } from 'nuxt/app'
import * as THREE from 'three'
import type { EffectComposer } from 'three/examples/jsm/postprocessing/EffectComposer.js'
import { OutlinePass } from 'three/examples/jsm/postprocessing/OutlinePass.js'
import { useRoute } from 'vue-router'
import { usePowerStationStore } from '~/stores/powerStation'
import type { HighlightConfig, RaycasterConfig } from './types'
// ============================================================
// 配置常量
// ============================================================
/** 悬停轮廓颜色 */
const HOVER_OUTLINE_COLOR = '#435c9d'
/** 点击轮廓颜色 */
const CLICK_OUTLINE_COLOR = '#9d7e43'
/** 轮廓通道配置 */
const OUTLINE_PASS_CONFIG = {
edgeStrength: 2,
edgeGlow: 1,
edgeThickness: 1,
} as const
/** 默认高亮配置 */
const DEFAULT_HIGHLIGHT_CONFIG: HighlightConfig = {
color: new THREE.Color(0x00ff00),
intensity: 0.5,
duration: 200,
}
/** 默认射线检测配置 */
const DEFAULT_RAYCASTER_CONFIG: RaycasterConfig = {
recursive: true,
threshold: 0.1,
}
// ============================================================
// 材质管理
// ============================================================
/** 存储原始材质用于恢复 */
const originalMaterials = new Map<string, THREE.Material | THREE.Material[]>()
/**
* 高亮物体(使用线框材质)
* @param object - 要高亮的物体
* @param config - 高亮配置
*/
const highlightObject = (
object: THREE.Object3D,
config: HighlightConfig = DEFAULT_HIGHLIGHT_CONFIG
) => {
object.traverse(child => {
if (child instanceof THREE.Mesh) {
const uuid = child.uuid
// 存储原始材质
if (!originalMaterials.has(uuid)) {
originalMaterials.set(uuid, child.material)
}
// 创建高亮材质
const highlightMaterial = new THREE.MeshBasicMaterial({
color: config.color,
transparent: true,
opacity: config.intensity,
wireframe: true,
})
child.material = highlightMaterial
}
})
}
/**
* 取消高亮物体(恢复原始材质)
* @param object - 要取消高亮的物体
*/
const unhighlightObject = (object: THREE.Object3D) => {
object.traverse(child => {
if (child instanceof THREE.Mesh) {
const uuid = child.uuid
const originalMaterial = originalMaterials.get(uuid)
if (originalMaterial) {
child.material = originalMaterial
originalMaterials.delete(uuid)
}
}
})
}
// ============================================================
// 主交互钩子
// ============================================================
/**
* 交互系统组合式函数
* @param getScene - 获取场景
* @param getCamera - 获取相机
* @param getComposer - 获取后期处理合成器
* @param getContainer - 获取容器元素
* @param modelGroup - 模型组
* @param getArchetypeModel - 获取原型模型
*/
export function useInteraction(
getScene: () => THREE.Scene,
getCamera: () => THREE.PerspectiveCamera,
getComposer: () => EffectComposer,
getContainer: () => HTMLElement | undefined,
modelGroup: THREE.Group,
getArchetypeModel: () => THREE.Object3D | null
) {
const store = usePowerStationStore()
const route = useRoute()
// 射线检测器
const raycaster = new THREE.Raycaster()
const mouse = new THREE.Vector2()
// 轮廓通道
let hoverOutlinePass: OutlinePass
let clickOutlinePass: OutlinePass
// 当前交互状态
let currentHoverObject: THREE.Object3D | null = null
let currentClickObject: THREE.Object3D | null = null
// ============================================================
// 射线检测
// ============================================================
/**
* 获取鼠标与场景物体的交点
* @param event - 鼠标事件
* @returns 交点数组
*/
const getIntersects = (event: MouseEvent): THREE.Intersection[] => {
const container = getContainer()
const camera = getCamera()
if (!container || !camera) return []
const rect = container.getBoundingClientRect()
mouse.x = ((event.clientX - rect.left) / rect.width) * 2 - 1
mouse.y = -((event.clientY - rect.top) / rect.height) * 2 + 1
raycaster.setFromCamera(mouse, camera)
return raycaster.intersectObjects(modelGroup.children, true)
}
/**
* 查找具有用户数据ID的父级物体
* @param object - 起始物体
* @returns 具有ID的物体或null
*/
const findObjectWithId = (object: THREE.Object3D): THREE.Object3D | null => {
let target = object
while (target && !target.userData.id && target !== modelGroup) {
if (target.parent) {
target = target.parent
} else {
break
}
}
return target?.userData.id ? target : null
}
// ============================================================
// 高亮效果
// ============================================================
/**
* 高亮悬停物体
* @param object - 要高亮的物体null清除高亮
*/
const highlightHover = (object: THREE.Object3D | null) => {
if (!hoverOutlinePass) return
if (object && object !== currentClickObject) {
if (currentHoverObject !== object) {
hoverOutlinePass.selectedObjects = [object]
currentHoverObject = object
}
} else {
hoverOutlinePass.selectedObjects = []
currentHoverObject = null
}
}
/**
* 高亮点击物体
* @param object - 要高亮的物体null清除高亮
*/
const highlightClick = (object: THREE.Object3D | null) => {
if (!clickOutlinePass) return
if (object) {
// 清除相同物体的悬停高亮
if (currentHoverObject === object) {
hoverOutlinePass.selectedObjects = []
currentHoverObject = null
}
clickOutlinePass.selectedObjects = [object]
currentClickObject = object
} else {
clickOutlinePass.selectedObjects = []
currentClickObject = null
}
}
// ============================================================
// 事件处理
// ============================================================
/**
* 点击事件处理
*/
const onClick = (event: MouseEvent) => {
const intersects = getIntersects(event)
if (intersects.length > 0 && intersects[0]) {
const target = findObjectWithId(intersects[0].object)
if (target && target.userData.id) {
const id = target.userData.id
const currentRouteNodeId = route.query.currentNodeId as string
const shouldHighlight = id !== currentRouteNodeId
// 更新store中的选中节点
store.selectNode({ id, label: target.name || `节点 ${id}` })
// 只有ID不一致时才高亮
if (shouldHighlight) {
highlightClick(target)
} else {
highlightClick(null)
}
}
} else {
// 点击空白区域,恢复到路由参数值
const currentRouteNodeId = route.query.currentNodeId as string
if (currentRouteNodeId) {
store.selectNode({ id: currentRouteNodeId, label: currentRouteNodeId })
} else {
store.selectNode({ id: '' })
}
highlightClick(null)
}
}
/**
* 双击事件处理
*/
const onDoubleClick = async (event: MouseEvent) => {
const intersects = getIntersects(event)
if (intersects.length > 0 && intersects[0]) {
const target = findObjectWithId(intersects[0].object)
if (target && target.userData.id) {
const id = target.userData.id
await navigateTo({
query: {
...route.query,
currentNodeId: id,
},
})
}
} else {
// 双击空白区域,返回父级
await handleNavigateToParent()
}
}
/**
* 导航到父级节点
*/
const handleNavigateToParent = async () => {
const currentRouteNodeId = route.query.currentNodeId as string
// 如果是根节点,不操作
if (!currentRouteNodeId || !currentRouteNodeId.includes('~')) {
return
}
// 删除最后一个"~"之后的内容
const lastUnderlineIndex = currentRouteNodeId.lastIndexOf('~')
if (lastUnderlineIndex > 0) {
const newCurrentNodeId = currentRouteNodeId.substring(0, lastUnderlineIndex)
await navigateTo({
query: {
...route.query,
currentNodeId: newCurrentNodeId,
},
})
store.selectNode({ id: newCurrentNodeId, label: newCurrentNodeId })
}
}
/**
* 鼠标移动事件处理
*/
const onMouseMove = (event: MouseEvent) => {
const intersects = getIntersects(event)
if (intersects.length > 0 && intersects[0]) {
const target = findObjectWithId(intersects[0].object)
highlightHover(target)
} else {
highlightHover(null)
}
}
// ============================================================
// 初始化与清理
// ============================================================
/**
* 初始化交互系统
*/
const initInteraction = () => {
const scene = getScene()
const camera = getCamera()
const composer = getComposer()
const container = getContainer()
if (!scene || !camera || !composer || !container) return
const width = container.clientWidth
const height = container.clientHeight
// 创建悬停轮廓通道
hoverOutlinePass = new OutlinePass(new THREE.Vector2(width, height), scene, camera)
hoverOutlinePass.edgeStrength = OUTLINE_PASS_CONFIG.edgeStrength
hoverOutlinePass.edgeGlow = OUTLINE_PASS_CONFIG.edgeGlow
hoverOutlinePass.edgeThickness = OUTLINE_PASS_CONFIG.edgeThickness
hoverOutlinePass.visibleEdgeColor.set(HOVER_OUTLINE_COLOR)
hoverOutlinePass.hiddenEdgeColor.set(HOVER_OUTLINE_COLOR)
composer.addPass(hoverOutlinePass)
// 创建点击轮廓通道
clickOutlinePass = new OutlinePass(new THREE.Vector2(width, height), scene, camera)
clickOutlinePass.edgeStrength = OUTLINE_PASS_CONFIG.edgeStrength
clickOutlinePass.edgeGlow = OUTLINE_PASS_CONFIG.edgeGlow
clickOutlinePass.edgeThickness = OUTLINE_PASS_CONFIG.edgeThickness
clickOutlinePass.visibleEdgeColor.set(CLICK_OUTLINE_COLOR)
clickOutlinePass.hiddenEdgeColor.set(CLICK_OUTLINE_COLOR)
composer.addPass(clickOutlinePass)
// 绑定事件
container.addEventListener('click', onClick)
container.addEventListener('dblclick', onDoubleClick)
container.addEventListener('mousemove', onMouseMove)
}
/**
* 更新轮廓通道尺寸
* @param width - 新宽度
* @param height - 新高度
*/
const updatePassSize = (width: number, height: number) => {
if (hoverOutlinePass) hoverOutlinePass.setSize(width, height)
if (clickOutlinePass) clickOutlinePass.setSize(width, height)
}
/**
* 清理资源
*/
const dispose = () => {
const container = getContainer()
if (container) {
container.removeEventListener('click', onClick)
container.removeEventListener('dblclick', onDoubleClick)
container.removeEventListener('mousemove', onMouseMove)
}
}
// ============================================================
// 导出接口
// ============================================================
return {
/** 初始化交互系统 */
initInteraction,
/** 高亮点击物体 */
highlightClick,
/** 高亮悬停物体 */
highlightHover,
/** 更新轮廓通道尺寸 */
updatePassSize,
/** 清理资源 */
dispose,
}
}