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) => {
|
2025-12-14 18:34:57 +08:00
|
|
|
|
// 取消鼠标悬停高亮效果
|
|
|
|
|
|
return
|
2025-12-11 01:29:41 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* 高亮点击物体
|
|
|
|
|
|
* @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) => {
|
2025-12-14 18:34:57 +08:00
|
|
|
|
// 取消鼠标悬停高亮效果,不再处理鼠标移动事件
|
|
|
|
|
|
return
|
2025-12-11 01:29:41 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// ============================================================
|
|
|
|
|
|
// 初始化与清理
|
|
|
|
|
|
// ============================================================
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* 初始化交互系统
|
|
|
|
|
|
*/
|
|
|
|
|
|
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)
|
2025-12-14 18:34:57 +08:00
|
|
|
|
// 取消鼠标移动事件监听,禁用悬停效果
|
|
|
|
|
|
// container.addEventListener('mousemove', onMouseMove)
|
2025-12-11 01:29:41 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* 更新轮廓通道尺寸
|
|
|
|
|
|
* @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)
|
2025-12-14 18:34:57 +08:00
|
|
|
|
// 鼠标移动事件监听器已移除,无需清理
|
|
|
|
|
|
// container.removeEventListener('mousemove', onMouseMove)
|
2025-12-11 01:29:41 +08:00
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// ============================================================
|
|
|
|
|
|
// 导出接口
|
|
|
|
|
|
// ============================================================
|
|
|
|
|
|
|
|
|
|
|
|
return {
|
|
|
|
|
|
/** 初始化交互系统 */
|
|
|
|
|
|
initInteraction,
|
|
|
|
|
|
/** 高亮点击物体 */
|
|
|
|
|
|
highlightClick,
|
|
|
|
|
|
/** 高亮悬停物体 */
|
|
|
|
|
|
highlightHover,
|
|
|
|
|
|
/** 更新轮廓通道尺寸 */
|
|
|
|
|
|
updatePassSize,
|
|
|
|
|
|
/** 清理资源 */
|
|
|
|
|
|
dispose,
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|