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

409 lines
12 KiB
TypeScript
Raw Normal View History

2025-12-11 01:29:41 +08:00
/**
*
*
*
*/
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,
}
}