You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
445 lines
12 KiB
445 lines
12 KiB
import * as THREE from 'three'
|
|
import type Viewport from '@/core/engine/Viewport'
|
|
import type { InteractionOption } from '@/core/manager/InteractionManager.ts'
|
|
import { getRenderer } from '@/core/manager/ModuleManager.ts'
|
|
import { Line2 } from 'three/examples/jsm/lines/Line2'
|
|
import { LineGeometry } from 'three/examples/jsm/lines/LineGeometry'
|
|
import { LineMaterial } from 'three/examples/jsm/lines/LineMaterial'
|
|
import { numberToString } from '@/utils/webutils.ts'
|
|
import { CSS2DObject } from 'three/examples/jsm/renderers/CSS2DRenderer'
|
|
|
|
let pdFn, pmFn, puFn
|
|
|
|
/**
|
|
* 基本交互控制器基类
|
|
* 定义了在建模编辑器中物流单元如何响应鼠标和键盘操作
|
|
*/
|
|
export default abstract class BaseInteraction {
|
|
viewport!: Viewport
|
|
canvas: HTMLCanvasElement
|
|
viewerDom: HTMLElement
|
|
|
|
// 交互选项
|
|
option: InteractionOption
|
|
|
|
// 临时标记
|
|
tempPointMarker?: THREE.Mesh
|
|
tempLine: Line2 | undefined = undefined
|
|
tempLabel: CSS2DObject | undefined = undefined
|
|
|
|
// 用于判断, 按下之后有没有移动
|
|
mouseOnlyClick = false
|
|
|
|
// 上次鼠标移动位置
|
|
lastMovePosition: THREE.Vector3 | undefined = undefined
|
|
|
|
// 保存上次点击时间,以便检测双击事件
|
|
lastClickTime: number = 0
|
|
|
|
readonly itemTypeName: string
|
|
|
|
// 连线起点
|
|
linkStartPointId: string
|
|
linkStartPointObject: THREE.Object3D
|
|
|
|
dragOption: DragOption | undefined
|
|
dragOriginPosition: THREE.Vector3 | undefined
|
|
dragItem: ItemJson | undefined
|
|
|
|
templineMaterial = new LineMaterial({
|
|
color: 0xE63C17, // 主颜色
|
|
linewidth: 2, // 实际可用的线宽
|
|
vertexColors: true, // 启用顶点颜色
|
|
dashed: false,
|
|
alphaToCoverage: true
|
|
})
|
|
|
|
/**
|
|
* 物品是否"单点", 不允许连线
|
|
*/
|
|
get isSinglePointMode(): boolean {
|
|
return false
|
|
}
|
|
|
|
constructor(itemTypeName: string) {
|
|
this.itemTypeName = itemTypeName
|
|
}
|
|
|
|
createPointOfItem(catchPoint: ItemJson, point: THREE.Vector3): ItemJson {
|
|
const renderer = getRenderer(this.itemTypeName)
|
|
|
|
const defaultScale = renderer.defaultScale
|
|
const defaultRotation = renderer.defaultRotation
|
|
|
|
_.extend(catchPoint, {
|
|
id: system.createUUID(),
|
|
t: this.itemTypeName,
|
|
v: true,
|
|
tf: [
|
|
[point.x, point.y, point.z],
|
|
[defaultRotation.x, defaultRotation.y, defaultRotation.z],
|
|
[defaultScale.x, defaultScale.y, defaultScale.z]
|
|
],
|
|
dt: {
|
|
in: [] as string[],
|
|
out: [] as string[],
|
|
center: [] as string[]
|
|
}
|
|
})
|
|
return catchPoint
|
|
}
|
|
|
|
/**
|
|
* 拖拽点开始
|
|
*/
|
|
dragPointStart(viewport: Viewport, dragOption: DragOption) {
|
|
this.viewport = viewport
|
|
this.dragOption = dragOption
|
|
this.dragOriginPosition = dragOption.object.position.clone()
|
|
|
|
// 找到 itemJson
|
|
const itemJson = _.find(this.viewport.stateManager.vdata.items, (item) => item.id === dragOption.entityId)
|
|
if (!itemJson) {
|
|
system.showErrorDialog('Not found for entityId:' + dragOption.entityId)
|
|
return false
|
|
}
|
|
|
|
this.dragItem = itemJson
|
|
return true
|
|
}
|
|
|
|
/**
|
|
* 拖拽点移动
|
|
*/
|
|
dragPointMove(viewport: Viewport, e: MouseEvent) {
|
|
if (this.viewport !== viewport) return
|
|
}
|
|
|
|
/**
|
|
* 拖拽点完成
|
|
*/
|
|
dragPointComplete(viewport: Viewport, e: MouseEvent) {
|
|
if (this.viewport !== viewport) return
|
|
|
|
// 获取当前鼠标所在位置
|
|
if (!CurrentMouseInfo || isNaN(CurrentMouseInfo.x) || isNaN(CurrentMouseInfo.z) || !this.dragItem?.tf?.[0]) {
|
|
return
|
|
}
|
|
|
|
// 提交状态管理器
|
|
const stateManager = this.viewport.stateManager
|
|
stateManager.beginStateUpdate({ createFromInteraction: true })
|
|
this.dragItem.tf[0][0] = CurrentMouseInfo.x
|
|
this.dragItem.tf[0][2] = CurrentMouseInfo.z
|
|
stateManager.endStateUpdate()
|
|
|
|
this.viewport = undefined
|
|
this.dragOption = undefined
|
|
this.dragItem = undefined
|
|
return true
|
|
}
|
|
|
|
/**
|
|
* 开始交互
|
|
*/
|
|
start(viewport: Viewport, option: InteractionOption = {}) {
|
|
this.stop()
|
|
this.viewport = viewport
|
|
this.option = option
|
|
|
|
this.viewerDom = this.viewport.viewerDom
|
|
this.canvas = this.viewport.renderer.domElement
|
|
if (option?.startPoint) {
|
|
this.linkStartPointId = option.startPoint
|
|
} else {
|
|
this.linkStartPointId = undefined
|
|
}
|
|
|
|
if (this.linkStartPointId) {
|
|
if (this.isSinglePointMode) {
|
|
// 单点模式不需要起始点
|
|
this.linkStartPointId = undefined
|
|
this.linkStartPointObject = undefined
|
|
|
|
} else {
|
|
this.linkStartPointObject = this.viewport.entityManager.findObjectsById(this.linkStartPointId)?.[0]
|
|
}
|
|
}
|
|
|
|
pdFn = this.mousedown.bind(this)
|
|
this.canvas.addEventListener('pointerdown', pdFn)
|
|
pmFn = this.mousemove.bind(this)
|
|
this.canvas.addEventListener('pointermove', pmFn)
|
|
puFn = this.mouseup.bind(this)
|
|
this.canvas.addEventListener('pointerup', puFn)
|
|
}
|
|
|
|
/**
|
|
* 停止交互
|
|
*/
|
|
stop() {
|
|
if (this.canvas) {
|
|
this.canvas.removeEventListener('pointerdown', pdFn)
|
|
pdFn = undefined
|
|
this.canvas.removeEventListener('pointermove', pmFn)
|
|
pmFn = undefined
|
|
this.canvas.removeEventListener('pointerup', puFn)
|
|
puFn = undefined
|
|
}
|
|
|
|
this.linkStartPointId = undefined
|
|
this.linkStartPointObject = undefined
|
|
|
|
// 清空所有临时点
|
|
this.tempPointMarker && this.viewport.scene.remove(this.tempPointMarker)
|
|
this.tempPointMarker = undefined
|
|
this.tempLine && this.viewport.scene.remove(this.tempLine)
|
|
this.tempLine = undefined
|
|
this.tempLabel && this.viewport.scene.remove(this.tempLabel)
|
|
this.tempLabel = undefined
|
|
|
|
this.viewerDom = undefined
|
|
this.canvas = undefined
|
|
}
|
|
|
|
mousedown() {
|
|
this.mouseOnlyClick = true
|
|
}
|
|
|
|
mousemove(e: MouseEvent): THREE.Vector3 | undefined {
|
|
this.mouseOnlyClick = false
|
|
|
|
// 当前鼠标所在的点
|
|
const point = this.viewport.getClosestIntersection(e)
|
|
if (!point) {
|
|
return
|
|
}
|
|
|
|
// 如果按下了 shift 键,则 point 的位置必须位于 startPoint 正上下方,或者正左右方
|
|
if (this.linkStartPointObject && e.shiftKey) {
|
|
const startPos = this.linkStartPointObject.position
|
|
const dx = Math.abs(point.x - startPos.x)
|
|
const dz = Math.abs(point.z - startPos.z)
|
|
if (dx > dz) {
|
|
point.z = startPos.z
|
|
} else {
|
|
point.x = startPos.x
|
|
}
|
|
}
|
|
|
|
// 移动时绘制临时线
|
|
if (this.linkStartPointObject) {
|
|
// 获取最后一个点
|
|
const p0 = this.linkStartPointObject.position
|
|
const dist = p0.distanceTo(point)
|
|
const label = `${numberToString(dist)} m`
|
|
const position = new THREE.Vector3().addVectors(p0, point).multiplyScalar(0.5)
|
|
|
|
if (!this.tempLine) {
|
|
this.tempLine = this.createTempLine()
|
|
this.viewport.scene.add(this.tempLine)
|
|
}
|
|
if (!this.tempLabel) {
|
|
this.tempLabel = this.createTempLabel(label)
|
|
this.viewport.scene.add(this.tempLabel)
|
|
}
|
|
|
|
this.tempLine.geometry.setFromPoints([p0, point])
|
|
this.tempLabel.position.set(position.x, position.y, position.z)
|
|
this.tempLabel.element.innerHTML = label
|
|
}
|
|
|
|
this.lastMovePosition = point
|
|
|
|
// 在鼠标移动时绘制临时点
|
|
if (this.tempPointMarker) {
|
|
this.tempPointMarker.position.set(point.x, point.y, point.z)
|
|
|
|
} else {
|
|
this.tempPointMarker = this.createTempPointMarker(point)
|
|
this.viewport.scene.add(this.tempPointMarker)
|
|
}
|
|
|
|
// this.viewport.dispatchSignal('sceneGraphChanged')
|
|
return point
|
|
}
|
|
|
|
/**
|
|
* 创建测量线
|
|
*/
|
|
createTempLine(): Line2 {
|
|
const geom = new LineGeometry()
|
|
const obj = new Line2(geom, this.templineMaterial)
|
|
obj.frustumCulled = false
|
|
return obj
|
|
}
|
|
|
|
/**
|
|
* 鼠标松开事件
|
|
*/
|
|
mouseup(e: MouseEvent) {
|
|
if (this.mouseOnlyClick) {
|
|
if (e.button === 2) {
|
|
// 右键点击, 完成绘图操作
|
|
this.viewport.interactionManager.exitInteraction()
|
|
|
|
} else if (e.button === 0) {
|
|
// 左键点击, 添加点
|
|
this.onMouseClicked(e)
|
|
}
|
|
}
|
|
}
|
|
|
|
onMouseClicked(e: MouseEvent): THREE.Vector3 | undefined {
|
|
// 获取鼠标点击位置的三维坐标
|
|
const point = this.lastMovePosition
|
|
if (!point) {
|
|
return
|
|
}
|
|
|
|
// 双击触发两次点击事件,我们需要避免这里的第二次点击
|
|
const now = Date.now()
|
|
if (this.lastClickTime && (now - this.lastClickTime < 50)) {
|
|
return
|
|
}
|
|
this.lastClickTime = now
|
|
|
|
|
|
// 如果正式的点命中到同类型的节点上,则不添加新的点,只牵线到该点
|
|
let catchPoint: ItemJson | null = this.viewport.stateManager.findItemByPosition(point, this.itemTypeName)
|
|
|
|
|
|
if (this.isSinglePointMode) {
|
|
// 单点模式,直接添加点
|
|
if (catchPoint) {
|
|
// 如果已经有点了,则不再添加
|
|
system.msg('Point already exists at this position.')
|
|
return
|
|
}
|
|
|
|
// 则添加一个新的点
|
|
const stateManager = this.viewport.stateManager
|
|
stateManager.beginStateUpdate({ createFromInteraction: true })
|
|
catchPoint = {} as ItemJson
|
|
catchPoint = this.createPointOfItem(catchPoint, point)
|
|
stateManager.vdata.items.push(catchPoint)
|
|
stateManager.endStateUpdate()
|
|
return
|
|
}
|
|
|
|
let from: ItemJson | undefined = undefined
|
|
if (this.linkStartPointId) {
|
|
from = this.viewport.stateManager.findItemById(this.linkStartPointId)
|
|
if (!from) {
|
|
system.showErrorDialog(`Cannot find state item: ${this.linkStartPointId}`)
|
|
return
|
|
}
|
|
}
|
|
|
|
if (catchPoint) {
|
|
// 连线到目标点
|
|
if (this.linkStartPointId === catchPoint.id) {
|
|
// 自己连接自己,忽略
|
|
system.msg('Cannot link to itself.')
|
|
return
|
|
}
|
|
// 关联2个点
|
|
|
|
if (this.linkStartPointId && from) {
|
|
catchPoint.dt.center.push(this.linkStartPointId)
|
|
from.dt.center.push(catchPoint.id)
|
|
}
|
|
|
|
// 提交状态管理器
|
|
const stateManager = this.viewport.stateManager
|
|
stateManager.beginStateUpdate({ createFromInteraction: true })
|
|
catchPoint.dt.center.push(this.linkStartPointId)
|
|
from.dt.center.push(catchPoint.id)
|
|
stateManager.endStateUpdate()
|
|
|
|
} else {
|
|
// 添加正式点
|
|
catchPoint = {} as ItemJson
|
|
catchPoint = this.createPointOfItem(catchPoint, point)
|
|
|
|
// 提交状态管理器
|
|
const stateManager = this.viewport.stateManager
|
|
stateManager.beginStateUpdate({ createFromInteraction: true })
|
|
// 关联2个点
|
|
stateManager.vdata.items.push(catchPoint)
|
|
if (from) {
|
|
catchPoint.dt.center.push(this.linkStartPointId)
|
|
from.dt.center.push(catchPoint.id)
|
|
|
|
} else {
|
|
stateManager.vdata.items.push(catchPoint)
|
|
}
|
|
stateManager.endStateUpdate()
|
|
}
|
|
|
|
// 把点加入拖拽控制器
|
|
// this.viewport.dragControl.setDragObjects(marker, 'push')
|
|
|
|
// 更新起始点为新添加的点
|
|
this.linkStartPointId = catchPoint.id
|
|
this.linkStartPointObject = this.viewport.entityManager.findObjectsById(catchPoint.id)?.[0]
|
|
|
|
// 删除临时点
|
|
this.tempPointMarker && this.viewport.scene.remove(this.tempPointMarker)
|
|
this.tempPointMarker = undefined
|
|
|
|
return point
|
|
}
|
|
|
|
|
|
/**
|
|
* 创建临时点标记
|
|
*/
|
|
createTempPointMarker(position?: THREE.Vector3): THREE.Mesh {
|
|
const p = position
|
|
const renderer = getRenderer(this.itemTypeName)
|
|
const scale = renderer.defaultScale
|
|
const rotation = renderer.defaultRotation
|
|
|
|
const geometry = new THREE.BoxGeometry(1, 1, 1)
|
|
const material = new THREE.MeshBasicMaterial({ color: 0x303133, transparent: true, opacity: 0.9 })
|
|
const obj = new THREE.Mesh(geometry, material)
|
|
obj.scale.set(scale.x, scale.y, scale.x)
|
|
obj.rotation.set(
|
|
THREE.MathUtils.degToRad(rotation.x),
|
|
THREE.MathUtils.degToRad(rotation.y),
|
|
THREE.MathUtils.degToRad(rotation.z)
|
|
)
|
|
if (p) {
|
|
obj.position.set(p.x, p.y, p.z)
|
|
}
|
|
return obj
|
|
}
|
|
|
|
/**
|
|
* 创建标签
|
|
*/
|
|
createTempLabel(text: string): CSS2DObject {
|
|
const div = document.createElement('div')
|
|
div.className = 'css2dObjectLabel'
|
|
div.innerHTML = text
|
|
div.style.padding = '5px 8px'
|
|
div.style.color = '#fff'
|
|
div.style.fontSize = '14px'
|
|
div.style.position = 'absolute'
|
|
div.style.backgroundColor = 'rgba(25, 25, 25, 0.3)'
|
|
div.style.borderRadius = '12px'
|
|
div.style.top = '0px'
|
|
div.style.left = '0px'
|
|
// div.style.pointerEvents = 'none' //避免HTML元素影响场景的鼠标事件
|
|
const obj = new CSS2DObject(div)
|
|
return obj
|
|
}
|
|
|
|
|
|
}
|
|
|
|
export interface DragOption {
|
|
object: THREE.Object3D
|
|
entityId: string
|
|
}
|