From 5487a6fcdc2d7130032fa490082eea32dcab94c5 Mon Sep 17 00:00:00 2001 From: yvan Date: Mon, 2 Jun 2025 17:02:30 +0800 Subject: [PATCH] =?UTF-8?q?BaseInteraction=20startInteraction=20=E5=AE=8C?= =?UTF-8?q?=E6=88=90?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/core/base/BaseInteraction.ts | 300 +++++++++++++++++++++++++++++- src/core/base/BaseRenderer.ts | 12 ++ src/core/manager/EntityManager.ts | 16 +- src/core/manager/InteractionManager.ts | 101 +++++++--- src/core/manager/StateManager.ts | 5 +- src/modules/measure/MeasureInteraction.ts | 14 +- src/modules/measure/MeasureRenderer.ts | 19 +- src/modules/measure/index.ts | 8 +- src/types/model.d.ts | 2 +- 9 files changed, 425 insertions(+), 52 deletions(-) diff --git a/src/core/base/BaseInteraction.ts b/src/core/base/BaseInteraction.ts index d3b1bb5..57207cf 100644 --- a/src/core/base/BaseInteraction.ts +++ b/src/core/base/BaseInteraction.ts @@ -1,35 +1,321 @@ 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 { - protected viewport!: Viewport + 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 + + templineMaterial = new LineMaterial({ + color: 0xE63C17, // 主颜色 + linewidth: 2, // 实际可用的线宽 + vertexColors: true, // 启用顶点颜色 + dashed: false, + alphaToCoverage: true + }) + + constructor(itemTypeName: string) { + this.itemTypeName = itemTypeName + } /** * 开始交互 - * @param viewport 当前视口 - * @param startPoint 起点对象(可选) */ - abstract start(viewport: Viewport, startPoint?: THREE.Object3D): void + 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.userData?.entityId + if (!this.linkStartPointId) { + 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) + } /** * 停止交互 */ - abstract stop(): void + 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 + } /** * 拖拽点开始 * @param viewport 当前视口 * @param point 拖拽的点 */ - abstract dragPointStart(viewport: Viewport, point: THREE.Object3D): void + dragPointStart(viewport: Viewport, point: THREE.Object3D) { + } /** * 拖拽点完成 * @param viewport 当前视口 */ - abstract dragPointComplete(viewport: Viewport): void + dragPointComplete(viewport: Viewport) { + } + + 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 + + const renderer = getRenderer(this.itemTypeName) + + const defaultScale = renderer.getDefaultScale() + const defaultRotation = renderer.getDefaultRotation() + + // 添加正式点 + const itemJson = { + 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[] + } + } as ItemJson + + // 关联2个点 + const fromItem = this.viewport.entityManager.findItemById(this.linkStartPointId) + if (this.linkStartPointId && fromItem) { + itemJson.dt.center.push(this.linkStartPointId) + fromItem.dt.center.push(itemJson.id) + } + + // 提交状态管理器 + const stateManager = this.viewport.stateManager + stateManager.beginStateUpdate({ createFromInteraction: true }) + stateManager.vdata.items.push(itemJson) + stateManager.endStateUpdate() + + // 把点加入拖拽控制器 + // this.viewport.dragControl.setDragObjects(marker, 'push') + + // 更新起始点为新添加的点 + this.linkStartPointId = itemJson.id + this.linkStartPointObject = this.viewport.entityManager.findObjectsById(itemJson.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.getDefaultScale() + const rotation = renderer.getDefaultRotation() + + 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 + } } \ No newline at end of file diff --git a/src/core/base/BaseRenderer.ts b/src/core/base/BaseRenderer.ts index c80d049..71a5b05 100644 --- a/src/core/base/BaseRenderer.ts +++ b/src/core/base/BaseRenderer.ts @@ -17,6 +17,12 @@ export default abstract class BaseRenderer { isUpdating: boolean = false + readonly itemTypeName: string + + constructor(itemTypeName: string) { + this.itemTypeName = itemTypeName + } + /** * 开始更新 * @param viewport 当前视口 @@ -44,6 +50,10 @@ export default abstract class BaseRenderer { */ abstract createLineBasic(start: ItemJson, end: ItemJson, type: LinkType): THREE.Object3D[] + abstract getDefaultScale(): THREE.Vector3 + + abstract getDefaultRotation(): THREE.Vector3 + /** * 创建或更新线之后的回调 */ @@ -122,6 +132,8 @@ export default abstract class BaseRenderer { this.fillObjectUserDataFromItem(item, ...points) this.tempViewport.entityManager.appendObject(item.id, points) this.appendToScene(...points) + + return points } diff --git a/src/core/manager/EntityManager.ts b/src/core/manager/EntityManager.ts index 0cde12b..a51ec4f 100644 --- a/src/core/manager/EntityManager.ts +++ b/src/core/manager/EntityManager.ts @@ -130,11 +130,11 @@ export default class EntityManager { } if (typeof originEntity === 'undefined') { - renderer.createPoint(entity, option) + renderer.createPoint(entity, option as RendererCudOption) } else { option.originEntity = _.cloneDeep(originEntity) - renderer.updatePoint(entity, option) + renderer.updatePoint(entity, option as RendererCudOption) } } @@ -152,7 +152,7 @@ export default class EntityManager { this.removeRelations(id) // 清理关系 this.entities.delete(id) // 删除实体 - this.getDiffRenderer(entity.t).deletePoint(id, option) + this.getDiffRenderer(entity.t).deletePoint(id, option as RendererCudOption) } generateLineDiffsForDelete(id: string): void { @@ -478,6 +478,16 @@ export default class EntityManager { deleteLineObjectOnly(id: string) { return this.lines.delete(id) } + + /** + * 根据 linkStartPointId 查找对应的 ItemJson + */ + findItemById(linkStartPointId: string): ItemJson | undefined { + if (!linkStartPointId) { + return + } + return this.entities.get(linkStartPointId) + } } interface LineDiffItem { diff --git a/src/core/manager/InteractionManager.ts b/src/core/manager/InteractionManager.ts index 2cda0f3..1f1f3c1 100644 --- a/src/core/manager/InteractionManager.ts +++ b/src/core/manager/InteractionManager.ts @@ -1,9 +1,16 @@ +import * as THREE from 'three' import type Viewport from '@/core/engine/Viewport.ts' import { watch } from 'vue' import type IControls from '@/core/controls/IControls.ts' import type BaseInteraction from '@/core/base/BaseInteraction.ts' import { getInteraction } from '@/core/manager/ModuleManager.ts' -import * as THREE from 'three' + +export interface InteractionOption { + /** + * 起点对象 + */ + startPoint?: THREE.Object3D +} /** * 交互管理器 @@ -22,29 +29,77 @@ export default class InteractionManager implements IControls { init(viewport: Viewport) { this.viewport = viewport - this.viewport.watchList.push(watch(() => this.viewport.state.cursorMode, (newVal: CursorMode) => { - const state = this.viewport.state - - if (!state.isReady) { - return - } - if (this.currentTool) { - this.currentTool.stop() - this.currentTool = null - } - if (newVal === 'normal' || !newVal) { - this.viewport.dragControl.dragControls.enabled = true - return - } - - this.currentTool = getInteraction(newVal) - this.viewport.dragControl.dragControls.enabled = false - - this.currentTool.start(this.viewport, this.toolStartObject) - this.toolStartObject = null - })) + this.viewport.watchList.push(watch(() => this.viewport.state.cursorMode, + (newVal: CursorMode, oldVal: CursorMode) => { + if (newVal === oldVal) return + this._setActiveInteraction(newVal) + })) + } + + /** + * 实现方法:启动交互 + */ + startInteraction(itemType: string, option: InteractionOption = {}): void { + this._setActiveInteraction(itemType, option.startPoint) + } + + /** + * 退出当前交互 + */ + exitInteraction() { + if (this.currentTool) { + this.currentTool.stop() + this.currentTool = null + } + + this.viewport.state.cursorMode = 'normal' + this.viewport.dragControl.dragControls.enabled = true + this.viewport.viewerDom.style.cursor = '' + system.msg('退出新建模式') + } + + private _setActiveInteraction(mode: string, startPoint?: THREE.Object3D): void { + const state = this.viewport.state + if (!state.isReady) return + + // 如果已经是 normal 模式,则退出交互 + if (mode === 'normal') { + this.exitInteraction() + return + } + + // 如果已有相同类型的交互,并且起点一致,无需重复初始化 + if (this.currentTool?.itemTypeName === mode && this.toolStartObject === startPoint) { + return + } + + // 清理之前的交互 + this.exitInteraction() + + // 获取新的交互实例 + const interaction = getInteraction(mode) + if (!interaction) { + system.msg(`not found '${mode}' interaction!`) + return + } + + // 保存参数 + if (startPoint) { + this.toolStartObject = startPoint + } + + // 初始化交互 + this.currentTool = interaction + this.viewport.dragControl.dragControls.enabled = false + this.currentTool.start(this.viewport, { startPoint: this.toolStartObject }) + + // 更新 UI 状态 + this.viewport.viewerDom.style.cursor = 'crosshair' + this.viewport.state.cursorMode = mode + system.msg(`enter [${mode}] interaction`) } - destory() { + destory(): void { + this.exitInteraction() } } \ No newline at end of file diff --git a/src/core/manager/StateManager.ts b/src/core/manager/StateManager.ts index c311a88..8ec2864 100644 --- a/src/core/manager/StateManager.ts +++ b/src/core/manager/StateManager.ts @@ -132,7 +132,7 @@ export default class StateManager { /** * 开始用户操作(创建数据快照) */ - beginStateUpdate(): void { + beginStateUpdate(option: StateUpdateOption = {}): void { this.lastStateDict = new Map(this.vdata.items.map(item => [item.id, _.cloneDeep(item)])) this.changeTracker.added.length = 0 this.changeTracker.removed.length = 0 @@ -534,3 +534,6 @@ export default class StateManager { } } +export interface StateUpdateOption { + createFromInteraction?: boolean +} diff --git a/src/modules/measure/MeasureInteraction.ts b/src/modules/measure/MeasureInteraction.ts index bd3f823..feb0971 100644 --- a/src/modules/measure/MeasureInteraction.ts +++ b/src/modules/measure/MeasureInteraction.ts @@ -1,18 +1,8 @@ -import * as THREE from 'three' -import type Viewport from '@/core/engine/Viewport.ts' import BaseInteraction from '@/core/base/BaseInteraction.ts' export default class MeasureInteraction extends BaseInteraction { - dragPointComplete(viewport: Viewport): void { - } - - dragPointStart(viewport: Viewport, point: THREE.Object3D): void { - } - start(viewport: Viewport, startPoint?: THREE.Object3D): void { + constructor(itemTypeName: string) { + super(itemTypeName) } - - stop(): void { - } - } \ No newline at end of file diff --git a/src/modules/measure/MeasureRenderer.ts b/src/modules/measure/MeasureRenderer.ts index 380a4b5..6b90931 100644 --- a/src/modules/measure/MeasureRenderer.ts +++ b/src/modules/measure/MeasureRenderer.ts @@ -24,9 +24,9 @@ export default class MeasureRenderer extends BaseRenderer { public useHtmlLabel = false - pointMaterial = new THREE.MeshBasicMaterial({ color: 0x303133, transparent: true, opacity: 0.9 }) + readonly pointMaterial = new THREE.MeshBasicMaterial({ color: 0x303133, transparent: true, opacity: 0.9 }) - lineMaterial = new LineMaterial({ + readonly lineMaterial = new LineMaterial({ color: 0xE63C17, // 主颜色 linewidth: 2, // 实际可用的线宽 vertexColors: true, // 启用顶点颜色 @@ -34,6 +34,13 @@ export default class MeasureRenderer extends BaseRenderer { alphaToCoverage: true }) + readonly defaultScale: THREE.Vector3 = new THREE.Vector3(0.25, 0.1, 0.25) + readonly defaultRotation: THREE.Vector3 = new THREE.Vector3(0, 0, 0) + + constructor(itemTypeName:string) { + super(itemTypeName) + } + createLineBasic(start: ItemJson, end: ItemJson, type: LinkType): THREE.Object3D[] { const geom = new LineGeometry() const obj = new Line2(geom, this.lineMaterial) @@ -152,4 +159,12 @@ export default class MeasureRenderer extends BaseRenderer { return label } } + + getDefaultScale(): THREE.Vector3 { + return this.defaultScale + } + + getDefaultRotation(): THREE.Vector3 { + return this.defaultRotation + } } \ No newline at end of file diff --git a/src/modules/measure/index.ts b/src/modules/measure/index.ts index 1bf7ec9..03e515a 100644 --- a/src/modules/measure/index.ts +++ b/src/modules/measure/index.ts @@ -4,10 +4,12 @@ import MeasureEntity from './MeasureEntity.ts' import MeasureMeta from './MeasureMeta.ts' import MeasureInteraction from './MeasureInteraction.ts' +export const ITEM_TYPE_NAME = 'measure' + defineModule({ - name: 'measure', - renderer: new MeasureRenderer(), - interaction: new MeasureInteraction(), + name: ITEM_TYPE_NAME, + renderer: new MeasureRenderer(ITEM_TYPE_NAME), + interaction: new MeasureInteraction(ITEM_TYPE_NAME), meta: MeasureMeta, entity: MeasureEntity }) \ No newline at end of file diff --git a/src/types/model.d.ts b/src/types/model.d.ts index a70d4a7..be482a2 100644 --- a/src/types/model.d.ts +++ b/src/types/model.d.ts @@ -12,7 +12,7 @@ interface InteractionCudOption { * 渲染器操作选项 */ interface RendererCudOption { - // Add any additional options needed for create, update, delete operations + createFromInteraction: boolean } /**