/** * 三维场景交互系统 * 处理鼠标悬停、点击、双击等交互事件 * 提供物体高亮和轮廓效果 */ 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() /** * 高亮物体(使用线框材质) * @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) => { // 取消鼠标悬停高亮效果 return } /** * 高亮点击物体 * @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) => { // 取消鼠标悬停高亮效果,不再处理鼠标移动事件 return } // ============================================================ // 初始化与清理 // ============================================================ /** * 初始化交互系统 */ 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, } }