Browse Source

RulerTool

master
修宁 7 months ago
parent
commit
c46ed5ca64
  1. 44
      src/designer/Viewport.ts
  2. 211
      src/designer/model2DEditor/DragControls.js
  3. 129
      src/designer/model2DEditor/EsDragControls.ts
  4. 4
      src/designer/model2DEditor/Model2DEditorJs.js
  5. 29
      src/designer/model2DEditor/tools/RulerInspect.ts
  6. 318
      src/designer/model2DEditor/tools/RulerTool.ts
  7. 12
      src/types/Types.d.ts
  8. 27
      src/utils/webutils.ts

44
src/designer/Viewport.ts

@ -2,13 +2,14 @@ import _ from 'lodash'
import * as THREE from 'three'
import { AxesHelper, GridHelper, OrthographicCamera, Raycaster, Scene, WebGLRenderer } from 'three'
import { OrbitControls } from 'three/examples/jsm/controls/OrbitControls'
import EsDragControls from './model2DEditor/EsDragControls'
import Stats from 'three/examples/jsm/libs/stats.module'
import type WorldModel from '@/designer/WorldModel.ts'
import $ from 'jquery'
import { reactive, watch } from 'vue'
import MouseMoveInspect from '@/designer/model2DEditor/tools/MouseMoveInspect.ts'
import type { ITool } from '@/designer/model2DEditor/tools/ITool.ts'
import RulerInspect from '@/designer/model2DEditor/tools/RulerInspect.ts'
import RulerTool from '@/designer/model2DEditor/tools/RulerTool.ts'
/**
*
@ -25,9 +26,10 @@ export default class Viewport {
controls: OrbitControls
worldModel: WorldModel
raycaster: Raycaster
dragControl: EsDragControls
animationFrameId: any = null
cursorTool: ITool | null = null
currentTool: ITool | null = null
tools: ITool[] = [
new MouseMoveInspect()
]
@ -62,6 +64,10 @@ export default class Viewport {
this.worldModel = worldModel
}
dispatchSignal(signal: string, data?: any) {
console.log('signal', signal, data)
}
/**
* THREE
*/
@ -141,10 +147,8 @@ export default class Viewport {
metalness: 0.9,
roughness: 0.1
})
const cube = new THREE.Mesh(geometry, material)
scene.add(cube)
this.camera.position.z = 5
this.animate()
@ -159,23 +163,26 @@ export default class Viewport {
if (!this.state.isReady) {
return
}
if (this.cursorTool) {
this.cursorTool.destory()
this.cursorTool = null
if (this.currentTool) {
this.currentTool.destory()
this.currentTool = null
}
if (newVal === 'normal' || !newVal) {
this.dragControl.dragControls.enabled = true
return
}
if (newVal === 'Ruler') {
if (newVal === 'Ruler' || newVal === 'RulerArea' || newVal === 'RulerAngle') {
// 选择标尺工具
this.cursorTool = new RulerInspect()
this.currentTool = new RulerTool()
this.dragControl.dragControls.enabled = false
} else {
system.showErrorDialog(`当前鼠标模式 ${newVal} 不支持`)
}
if (this.cursorTool) {
this.cursorTool.init(this)
if (this.currentTool) {
this.currentTool.init(this)
}
}))
@ -193,6 +200,9 @@ export default class Viewport {
tool.init(this)
}
// 注册拖拽组件
this.dragControl = new EsDragControls(this)
this.state.isReady = true
}
@ -383,7 +393,15 @@ export default class Viewport {
getGridHelpAtPosition(param: { x: number; z: number }) {
const pickPosition = new THREE.Vector2(param.x, param.z)
this.raycaster.setFromCamera(pickPosition, this.camera)
return this.raycaster.intersectObject(this.gridHelper)
return this.raycaster.intersectObject(this.gridHelper, false)
}
getIntersects(point: THREE.Vector2) {
const mouse = new THREE.Vector2()
mouse.set((point.x * 2) - 1, -(point.y * 2) + 1)
this.raycaster.setFromCamera(mouse, this.camera)
return this.raycaster.intersectObjects([this.gridHelper], false)
}
}
@ -401,7 +419,7 @@ export interface ViewportState {
/**
*
*/
cursorMode: 'normal' | 'ALink' | 'SLink' | 'PointCallback' | 'PointAdd' | 'LinkAdd' | 'LinkAdd2' | 'Ruler' | 'selectByRec',
cursorMode: CursorMode,
/**
*

211
src/designer/model2DEditor/DragControls.js

@ -0,0 +1,211 @@
import {
EventDispatcher,
Matrix4,
Plane,
Raycaster,
Vector2,
Vector3
} from 'three';
const _plane = new Plane();
const _raycaster = new Raycaster();
const _pointer = new Vector2();
const _offset = new Vector3();
const _intersection = new Vector3();
const _worldPosition = new Vector3();
const _inverseMatrix = new Matrix4();
class DragControls extends EventDispatcher {
constructor( _objects, _camera, _domElement ) {
super();
_domElement.style.touchAction = 'none'; // disable touch scroll
let _selected = null, _hovered = null;
const _intersections = [];
//
let isMove = false;
const scope = this;
function activate() {
_domElement.addEventListener( 'pointermove', onPointerMove );
_domElement.addEventListener( 'pointerdown', onPointerDown );
_domElement.addEventListener( 'pointerup', onPointerCancel );
_domElement.addEventListener( 'pointerleave', onPointerCancel );
}
function deactivate() {
_domElement.removeEventListener( 'pointermove', onPointerMove );
_domElement.removeEventListener( 'pointerdown', onPointerDown );
_domElement.removeEventListener( 'pointerup', onPointerCancel );
_domElement.removeEventListener( 'pointerleave', onPointerCancel );
_domElement.style.cursor = '';
}
function dispose() {
deactivate();
}
function setObjects( objects ) {
_objects = objects;
}
function getObjects() {
return _objects;
}
function getRaycaster() {
return _raycaster;
}
function onPointerMove( event ) {
if ( !scope.enabled || !scope.enabledMove) return;
isMove = true;
updatePointer( event );
_raycaster.setFromCamera( _pointer, _camera );
if ( _selected ) {
if ( _raycaster.ray.intersectPlane( _plane, _intersection ) ) {
_selected.position.copy( _intersection.sub( _offset ).applyMatrix4( _inverseMatrix ) );
}
scope.dispatchEvent( { type: 'drag', object: _selected } );
return;
}
// hover support
if ( event.pointerType === 'mouse' || event.pointerType === 'pen' ) {
_intersections.length = 0;
_raycaster.setFromCamera( _pointer, _camera );
_raycaster.intersectObjects( _objects, true, _intersections );
if ( _intersections.length > 0 ) {
const object = _intersections[ 0 ].object;
_plane.setFromNormalAndCoplanarPoint( _camera.getWorldDirection( _plane.normal ), _worldPosition.setFromMatrixPosition( object.matrixWorld ) );
if ( _hovered !== object && _hovered !== null ) {
scope.dispatchEvent( { type: 'hoveroff', object: _hovered } );
_domElement.style.cursor = 'auto';
_hovered = null;
}
if ( _hovered !== object ) {
scope.dispatchEvent( { type: 'hoveron', object: object } );
_domElement.style.cursor = 'pointer';
_hovered = object;
}
} else {
if ( _hovered !== null ) {
scope.dispatchEvent( { type: 'hoveroff', object: _hovered } );
_domElement.style.cursor = 'auto';
_hovered = null;
}
}
}
}
function onPointerDown( event ) {
if (scope.enabled === false) return;
updatePointer(event);
_intersections.length = 0;
_raycaster.setFromCamera( _pointer, _camera );
let objects = _objects;
_raycaster.intersectObjects( objects, true, _intersections );
if (_intersections.length > 0) {
_selected = (scope.transformGroup === true) ? _objects[ 0 ] : _intersections[0].object;
if(scope.enabledMove) {
_plane.setFromNormalAndCoplanarPoint(_camera.getWorldDirection(_plane.normal), _worldPosition.setFromMatrixPosition(_selected.matrixWorld));
if (_raycaster.ray.intersectPlane(_plane, _intersection)) {
_inverseMatrix.copy(_selected.parent.matrixWorld).invert();
_offset.copy(_intersection).sub(_worldPosition.setFromMatrixPosition(_selected.matrixWorld));
}
_domElement.style.cursor = 'move';
}
scope.dispatchEvent( { type: 'dragstart', object: _selected,e:event } );
}
isMove = false;
}
function onPointerCancel(event) {
if ( scope.enabled === false ) return;
if ( _selected ) {
scope.dispatchEvent( { type: 'dragend', object: _selected,e:event } );
_selected = null;
}else if(!isMove){
// 添加点击空白处的事件
scope.dispatchEvent( { type: 'clickblank',e:event } );
}
_domElement.style.cursor = _hovered ? 'pointer' : 'auto';
}
function updatePointer( event ) {
const rect = _domElement.getBoundingClientRect();
_pointer.x = ( event.clientX - rect.left ) / rect.width * 2 - 1;
_pointer.y = - ( event.clientY - rect.top ) / rect.height * 2 + 1;
}
activate();
// API
this.enabled = true;
this.enabledMove = true;
this.transformGroup = false;
this.activate = activate;
this.deactivate = deactivate;
this.dispose = dispose;
this.setObjects = setObjects;
this.getObjects = getObjects;
this.getRaycaster = getRaycaster;
}
}
export { DragControls };

129
src/designer/model2DEditor/EsDragControls.ts

@ -0,0 +1,129 @@
import { DragControls } from './DragControls.js'
import * as THREE from 'three'
import type Viewport from '@/designer/Viewport.ts'
// dragControls 绑定函数
let dragStartFn, dragFn, dragEndFn, clickblankFn
export default class EsDragControls {
protected _dragObjects: THREE.Object3D[] = [] // 拖拽对象
dragControls: any
private onDownPosition: { x: number; y: number } = { x: -1, y: -1 }
viewport: Viewport
isDragging = false
constructor(viewport) {
this.viewport = viewport
// 物体拖拽控制器
this.dragControls = new DragControls(this._dragObjects, viewport.camera, viewport.renderer.domElement)
this.dragControls.deactivate() // 默认禁用
dragStartFn = this.dragControlsStart.bind(this)
this.dragControls.addEventListener('dragstart', dragStartFn)
dragFn = this.drag.bind(this)
this.dragControls.addEventListener('drag', dragFn)
dragEndFn = this.dragControlsEnd.bind(this)
this.dragControls.addEventListener('dragend', dragEndFn)
// 点击可拖拽物体之外
clickblankFn = this.clickblank.bind(this)
this.dragControls.addEventListener('clickblank', clickblankFn)
}
set domElement(element: HTMLElement) {
this.dragControls.setDomElement(element)
}
setDragObjects(objects: THREE.Object3D[], type: 'eq' | 'push' | 'remove' = 'eq') {
// 当前拖拽对象为空时加入对象需激活控制器
if (this._dragObjects.length === 0) {
if (objects.length > 0) {
this.dragControls.activate()
}
this._dragObjects = objects
} else {
// 当前拖拽对象不为空时
if (type === 'eq') {
// 是清空拖拽对象的设置,则禁用控制器
if (objects.length === 0) {
this.dragControls.deactivate()
}
this._dragObjects = objects
} else if (type === 'push') {
this._dragObjects.push(...objects)
} else if (type === 'remove') {
this._dragObjects = this._dragObjects.filter((item) => !objects.includes(item))
}
}
this.dragControls.setObjects(this._dragObjects)
}
// 拖拽开始
dragControlsStart(e) {
// 右键拖拽不响应
if (e.e.button === 2 || !e.object.userData.type || !e.object.visible) return
e.e.preventDefault()
// 拖拽时禁用其他控制器
this.viewport.controls.enabled = false
this.isDragging = true
// 记录拖拽按下的位置和对象
this.onDownPosition = { x: e.e.clientX, y: e.e.clientY }
// switch (e.object.userData.type) {
// case 'measure-marker':
// this.viewport.modules.measure.redraw(e.object)
// break
// }
}
// 拖拽中
drag(e) {
this.viewport.dispatchSignal('objectChanged', e.object)
}
// 拖拽结束
dragControlsEnd(e) {
// 右键拖拽不响应
if (e.e.button === 2 || !e.object.visible) return
// 拖拽结束启用其他控制器
this.viewport.controls.enabled = true
this.isDragging = false
if (!e.object.userData.type) return
// 判断位置是否有变化,没有变化则为点击
if (this.onDownPosition.x === e.e.clientX && this.onDownPosition.y === e.e.clientY) {
if (e.object.userData.onClick) {
e.object.userData.onClick(e)
}
}
// switch (e.object.userData.type) {
// case 'measure-marker':
// this.viewport.modules.measure.redrawComplete()
// break
// }
}
// 点击可拖拽物体之外
clickblank(e) {
if (e.e.button === 2) return
}
dispose() {
this._dragObjects = []
this.dragControls.removeEventListener('dragstart', dragStartFn)
this.dragControls.removeEventListener('dragend', dragEndFn)
this.dragControls.dispose()
}
}

4
src/designer/model2DEditor/Model2DEditorJs.js

@ -15,11 +15,11 @@ export default defineComponent({
}
},
mounted() {
window['editor'] = this
window['viewport'] = this
},
beforeMount() {
this.initByFloor('')
delete window['editor']
delete window['viewport']
},
methods: {
renderIcon,

29
src/designer/model2DEditor/tools/RulerInspect.ts

@ -1,29 +0,0 @@
import type { ITool } from '@/designer/model2DEditor/tools/ITool.ts'
import Viewport from '@/designer/Viewport.ts'
export default class RulerInspect implements ITool {
viewport: Viewport
mouseMove(event: MouseEvent) {
const viewer = document.querySelector('.viewer') as HTMLElement
const rect = viewer.getBoundingClientRect()
const mouseX = event.clientX - rect.left
const mouseY = event.clientY - rect.top
console.log(`Mouse Position: (${mouseX}, ${mouseY})`)
}
init(viewport: Viewport) {
this.viewport = viewport
const viewerDom = this.viewport.viewerDom
system.msg('进入鼠标测距模式')
}
destory(): void {
const viewerDom = this.viewport.viewerDom
viewerDom.removeEventListener('mousemove', this.mouseMove)
system.msg('退出鼠标测距模式')
}
}

318
src/designer/model2DEditor/tools/RulerTool.ts

@ -0,0 +1,318 @@
import _ from 'lodash'
import PointPng from '@/assets/images/logo.png'
import * as THREE from 'three'
import type { ITool } from '@/designer/model2DEditor/tools/ITool.ts'
import Viewport from '@/designer/Viewport.ts'
import { numberToString, getUnitString } from '@/utils/webutils'
import { CSS2DObject } from 'three/examples/jsm/renderers/CSS2DRenderer'
import { Vector3 } from 'three'
let pdFn, pmFn, puFn, kdFn
export default class RulerTool implements ITool {
static OBJ_NAME = 'object_for_measure'
static LABEL_NAME = 'label_for_measure'
static MAX_DISTANCE = 500 //当相交物体的距离太远时,忽略它
viewport: Viewport
// 当前测绘内容组
group: THREE.Group
isCompleted = false
mouseMoved = false
canvas: HTMLCanvasElement
// 用户在测量时绘制的线的当前实例
protected polyline?: THREE.Line
// 用于存储临时点
protected tempPointMarker?: THREE.Mesh
// 用于存储临时线条,用于在鼠标移动时绘制线条/区域/角度
protected tempLine?: THREE.Line
// 用于在鼠标移动时存储临时标签,只有测量距离时才有
protected tempLabel?: CSS2DObject
// 存储点
protected pointArray: THREE.Vector3[] = []
//保存上次点击时间,以便检测双击事件
protected lastClickTime: number = 0
static LINE_MATERIAL = new THREE.LineBasicMaterial({
color: 0xE63C17,
linewidth: 2,
opacity: 0.9,
transparent: true,
side: THREE.DoubleSide,
depthWrite: false,
depthTest: false
})
init(viewport: Viewport) {
this.viewport = viewport
const viewerDom = this.viewport.viewerDom
this.canvas = $(viewerDom).children('canvas')[0] as HTMLCanvasElement
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)
// 初始化group
this.group = new THREE.Group()
this.group.name = `${RulerTool.OBJ_NAME}_group`
this.group.userData = {
mode: this.viewport.state.cursorMode
}
this.viewport.scene.add(this.group)
// 测量距离、面积和角度需要折线
this.polyline = this.createLine()
this.group.add(this.polyline)
this.isCompleted = false
this.viewport.viewerDom.style.cursor = 'crosshair'
// 当次绘制点
this.pointArray = []
// 测量距离、面积和角度需要折线
this.polyline = this.createLine()
this.group.add(this.polyline)
this.viewport.viewerDom.style.cursor = 'crosshair'
system.msg('进入鼠标测距模式')
}
destory(): void {
system.msg('退出鼠标测距模式')
const viewerDom = this.viewport.viewerDom
this.isCompleted = true
viewerDom.style.cursor = ''
this.canvas.removeEventListener('pointerdown', pdFn)
pdFn = undefined
this.canvas.removeEventListener('pointermove', pmFn)
pmFn = undefined
this.canvas.removeEventListener('pointerup', puFn)
puFn = undefined
this.tempPointMarker && this.viewport.scene.remove(this.tempPointMarker)
this.tempLine && this.viewport.scene.remove(this.tempLine)
this.tempLabel && this.viewport.scene.remove(this.tempLabel)
this.tempPointMarker = undefined
this.tempLine = undefined
this.tempLabel = undefined
}
mousedown = () => {
this.mouseMoved = false
}
// 鼠标移动,创建对应的临时点与线
mousemove = (e: MouseEvent) => {
if (this.isCompleted) return
this.mouseMoved = true
const point = this.getClosestIntersection(e)
if (!point) {
return
}
// 在鼠标移动时绘制临时点
if (this.tempPointMarker) {
this.tempPointMarker.position.set(point.x, point.y, point.z)
} else {
this.tempPointMarker = this.createPointMarker(point)
this.viewport.scene.add(this.tempPointMarker)
}
// 移动时绘制临时线
if (this.pointArray.length > 0) {
const p0 = this.pointArray[this.pointArray.length - 1] // 获取最后一个点
const line = this.tempLine || this.createLine()
const geom = line.geometry
const startPoint = this.pointArray[0]
const lastPoint = this.pointArray[this.pointArray.length - 1]
if (this.viewport.state.cursorMode === 'RulerArea') {
geom.setFromPoints([lastPoint, point, startPoint])
} else {
geom.setFromPoints([lastPoint, point])
}
if (this.viewport.state.cursorMode === 'Ruler') {
const dist = p0.distanceTo(point)
const label = `${numberToString(dist)} ${getUnitString(this.viewport.state.cursorMode)}`
const position = new THREE.Vector3((point.x + p0.x) / 2, (point.y + p0.y) / 2, (point.z + p0.z) / 2)
this.addOrUpdateTempLabel(label, position)
}
// tempLine 只需添加到场景一次
if (!this.tempLine) {
this.viewport.scene.add(line)
this.tempLine = line
}
}
// this.viewport.dispatchSignal('sceneGraphChanged')
}
mouseup = (e: MouseEvent) => {
// 如果mouseMoved是true,那么它可能在移动,而不是点击
if (!this.mouseMoved) {
// 右键点击表示完成绘图操作
if (e.button === 2) {
this.viewport.state.cursorMode = 'normal'
} else if (e.button === 0) { // 左键点击表示添加点
this.onMouseClicked(e)
}
}
}
onMouseClicked = (e: MouseEvent) => {
if (this.isCompleted) {
return
}
const point = this.getClosestIntersection(e)
if (!point) {
return
}
// 双击触发两次点击事件,我们需要避免这里的第二次点击
const now = Date.now()
if (this.lastClickTime && (now - this.lastClickTime < 100)) return
this.lastClickTime = now
this.pointArray.push(point)
console.log('pointArray', this.pointArray)
const count = this.pointArray.length
const marker = this.createPointMarker(point)
marker.userData.point = point
marker.userData.pointIndex = count - 1
this.group.add(marker)
// 把点加入拖拽控制器
this.viewport.dragControl.setDragObjects([marker], 'push')
if (this.polyline) {
this.polyline.geometry.setFromPoints(this.pointArray)
if (this.tempLabel && count > 1) {
const p0 = this.pointArray[count - 2]
this.tempLabel.position.set((p0.x + point.x) / 2, (p0.y + point.y) / 2, (p0.z + point.z) / 2)
this.group.add(this.tempLabel)
// 创建距离测量线时,此处的 临时label 将作为正式的使用,不在this.clearTemp()中清除,故置为undefined
this.tempLabel = undefined
}
}
// this.viewport.dispatchSignal('sceneGraphChanged')
}
/**
* Creates THREE.Line
*/
private createLine(): THREE.Line {
const geom = new THREE.BufferGeometry()
const obj = new THREE.Line(geom, RulerTool.LINE_MATERIAL)
obj.frustumCulled = false
obj.name = RulerTool.OBJ_NAME
obj.userData = {
type: 'line'
}
return obj
}
/**
*
*/
createPointMarker(position?: THREE.Vector3): THREE.Mesh {
const p = position
const scale = 0.25
const tt = new THREE.BoxGeometry(1, 1, 1)
const t2 = new THREE.MeshBasicMaterial({ color: 0x303133, transparent: true, opacity: 0.9 })
const obj = new THREE.Mesh(tt, t2)
obj.scale.set(scale, 0.1, scale)
if (p) {
obj.position.set(p.x, p.y, p.z)
}
obj.name = RulerTool.OBJ_NAME
obj.userData = {
mode: this.viewport.state.cursorMode,
type: 'measure-marker'
}
return obj
}
/**
*
*/
getClosestIntersection(e: MouseEvent) {
const _point = new THREE.Vector2()
_point.x = e.offsetX / this.viewport.renderer.domElement.offsetWidth
_point.y = e.offsetY / this.viewport.renderer.domElement.offsetHeight
const intersects = this.viewport.getIntersects(_point)
if (intersects && intersects.length > 2) {
if (intersects.length > 0 && intersects[0].distance < RulerTool.MAX_DISTANCE) {
return new Vector3(
intersects[0].point.x,
0.1,
intersects[1].point.z
)
}
}
return null
}
/**
*
*/
addOrUpdateTempLabel(label: string, position: THREE.Vector3) {
if (!this.tempLabel) {
this.tempLabel = this.createLabel(label)
this.viewport.scene.add(this.tempLabel)
}
this.tempLabel.position.set(position.x, position.y, position.z)
this.tempLabel.element.innerHTML = label
}
/**
*
*/
createLabel(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)
obj.name = RulerTool.LABEL_NAME
obj.userData = {
type: 'label'
}
return obj
}
}

12
src/types/Types.d.ts

@ -0,0 +1,12 @@
type CursorMode =
'normal'
| 'ALink'
| 'SLink'
| 'PointCallback'
| 'PointAdd'
| 'LinkAdd'
| 'LinkAdd2'
| 'Ruler'
| 'RulerArea'
| 'RulerAngle'
| 'selectByRec'

27
src/utils/webutils.ts

@ -5,6 +5,33 @@ import { ElIcon } from 'element-plus'
import * as FaIcon from '@vicons/fa'
import * as ElementPlusIconsVue from '@element-plus/icons-vue'
/**
*
*/
export function numberToString(num: number) {
if (num < 0.0001) {
return num.toString();
}
let fractionDigits = 2;
if (num < 0.01) {
fractionDigits = 4;
} else if (num < 0.1) {
fractionDigits = 3;
}
return num.toFixed(fractionDigits);
}
/**
*
*/
export function getUnitString(mode: CursorMode): string {
if (mode === 'Ruler') return "m";
if (mode === 'RulerArea') return "m²";
if (mode === 'RulerAngle') return "°";
return "";
}
export function normalizeShortKey(key: string): string {
// 如果 menu.tip 中包含 ctrl/shift/alt/key- 等修饰键,并且 click 事件存在,则需要使用 hotkeys 绑定
if (key && /ctrl|shift|alt|key-/i.test(_.toLower(key))) {

Loading…
Cancel
Save