init5
This commit is contained in:
408
app/composables/powerStation/useInteraction.ts
Normal file
408
app/composables/powerStation/useInteraction.ts
Normal file
@@ -0,0 +1,408 @@
|
||||
/**
|
||||
* 三维场景交互系统
|
||||
* 处理鼠标悬停、点击、双击等交互事件
|
||||
* 提供物体高亮和轮廓效果
|
||||
*/
|
||||
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,
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user