365 lines
10 KiB
Vue
365 lines
10 KiB
Vue
|
|
<template>
|
|||
|
|
<div class="canvas-container" :style="{'background-color': isConnected ? 'rgba(4, 33, 58, 70%)' : '#fff'}">
|
|||
|
|
<div ref="container" class="point-cloud-container"></div>
|
|||
|
|
<!-- <div v-if="!isConnected" class="reload_bg"><el-button type="danger">刷新</el-button></div> -->
|
|||
|
|
</div>
|
|||
|
|
</template>
|
|||
|
|
|
|||
|
|
<script>
|
|||
|
|
import * as THREE from 'three'
|
|||
|
|
import { OrbitControls } from 'three/examples/jsm/controls/OrbitControls'
|
|||
|
|
import { mapGetters } from 'vuex'
|
|||
|
|
import { points } from '../../config/point.js'
|
|||
|
|
export default {
|
|||
|
|
/* eslint-disable */
|
|||
|
|
data () {
|
|||
|
|
return {
|
|||
|
|
isConnected: true,
|
|||
|
|
points: [], // 存储所有点云数据
|
|||
|
|
ws: null, // WebSocket 实例
|
|||
|
|
// Three.js 相关对象
|
|||
|
|
scene: null,
|
|||
|
|
camera: null,
|
|||
|
|
renderer: null,
|
|||
|
|
controls: null,
|
|||
|
|
pointCloud: null,
|
|||
|
|
vehicleSprite: null,
|
|||
|
|
// 渲染循环
|
|||
|
|
animationFrameId: null,
|
|||
|
|
// 性能优化
|
|||
|
|
pointsBuffer: [],
|
|||
|
|
bufferUpdateThreshold: 500,
|
|||
|
|
lastUpdateTime: 0,
|
|||
|
|
// 小车图片
|
|||
|
|
vehicleImage: require('../../images/new/agv.png'),
|
|||
|
|
// 2D视图参数
|
|||
|
|
viewSize: 20,
|
|||
|
|
minZoom: 0.5,
|
|||
|
|
maxZoom: 5
|
|||
|
|
}
|
|||
|
|
},
|
|||
|
|
computed: {
|
|||
|
|
...mapGetters(['agvObj', 'position', 'rotation']),
|
|||
|
|
},
|
|||
|
|
watch: {
|
|||
|
|
agvObj: {
|
|||
|
|
handler() {
|
|||
|
|
this.updateVehiclePosition()
|
|||
|
|
},
|
|||
|
|
deep: true
|
|||
|
|
},
|
|||
|
|
position: {
|
|||
|
|
handler() {
|
|||
|
|
this.updateVehiclePosition()
|
|||
|
|
},
|
|||
|
|
deep: true
|
|||
|
|
},
|
|||
|
|
rotation: {
|
|||
|
|
handler() {
|
|||
|
|
this.updateVehiclePosition()
|
|||
|
|
},
|
|||
|
|
deep: true
|
|||
|
|
}
|
|||
|
|
},
|
|||
|
|
mounted () {
|
|||
|
|
this.initThreeJS()
|
|||
|
|
// this.initWebSocket()
|
|||
|
|
this.init()
|
|||
|
|
this.startRendering()
|
|||
|
|
window.addEventListener('resize', this.handleResize)
|
|||
|
|
},
|
|||
|
|
beforeDestroy () {
|
|||
|
|
if (this.ws) this.ws.close()
|
|||
|
|
window.removeEventListener('resize', this.handleResize)
|
|||
|
|
if (this.renderer) this.renderer.dispose()
|
|||
|
|
if (this.controls) {
|
|||
|
|
this.controls.dispose()
|
|||
|
|
}
|
|||
|
|
},
|
|||
|
|
methods: {
|
|||
|
|
initWebSocket() {
|
|||
|
|
const wsHost = process.env.VUE_APP_API_BASE_URL.replace(/^https?:\/\//, '')
|
|||
|
|
const sid = this.$store.getters.userInfo === 'true' ? 1 : 2
|
|||
|
|
this.ws = new WebSocket(`ws://${wsHost}/webSocket/PointCloudData/${sid}`)
|
|||
|
|
|
|||
|
|
this.ws.onopen = () => {
|
|||
|
|
console.log('WebSocket connected')
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
this.ws.onmessage = (event) => {
|
|||
|
|
const newPoints = event.data
|
|||
|
|
// 确保所有点都在XY平面(z=0)
|
|||
|
|
const points2D = newPoints.map(p => ({ x: p.x, y: p.y, z: 0 }))
|
|||
|
|
this.processNewPoints(points2D)
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
this.ws.onerror = (error) => {
|
|||
|
|
console.error('WebSocket error:', error)
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
this.ws.onclose = () => {
|
|||
|
|
console.log('WebSocket disconnected')
|
|||
|
|
}
|
|||
|
|
},
|
|||
|
|
|
|||
|
|
init () {
|
|||
|
|
this.isConnected = true
|
|||
|
|
const newPoints = points.data
|
|||
|
|
const points2D = newPoints.map(p => ({ x: p.x, y: p.y, z: 0 }))
|
|||
|
|
this.processNewPoints(points2D)
|
|||
|
|
},
|
|||
|
|
|
|||
|
|
initThreeJS() {
|
|||
|
|
// viewSize: 控制视图范围大小,值越小看到的范围越小(视角越近)
|
|||
|
|
// camera.position.z: 相机Z轴位置,值越小视角越近
|
|||
|
|
// camera.zoom: 直接控制缩放级别,大于1放大,小于1缩小
|
|||
|
|
// 1. 创建场景
|
|||
|
|
this.scene = new THREE.Scene()
|
|||
|
|
|
|||
|
|
// 2. 创建正交相机(2D视图)
|
|||
|
|
const container = this.$refs.container
|
|||
|
|
const aspect = container.clientWidth / container.clientHeight
|
|||
|
|
|
|||
|
|
this.camera = new THREE.OrthographicCamera(
|
|||
|
|
-this.viewSize * aspect / 2,
|
|||
|
|
this.viewSize * aspect / 2,
|
|||
|
|
this.viewSize / 2,
|
|||
|
|
-this.viewSize / 2,
|
|||
|
|
0.1,
|
|||
|
|
1000
|
|||
|
|
)
|
|||
|
|
this.camera.position.set(0, 0, 40)
|
|||
|
|
this.camera.lookAt(0, 0, 0)
|
|||
|
|
|
|||
|
|
// 3.创建渲染器 - 启用alpha通道实现透明背景
|
|||
|
|
this.renderer = new THREE.WebGLRenderer({
|
|||
|
|
antialias: true,
|
|||
|
|
alpha: true, // 关键:启用alpha通道
|
|||
|
|
transparent: true // 关键:允许透明
|
|||
|
|
});
|
|||
|
|
this.renderer.setPixelRatio(window.devicePixelRatio)
|
|||
|
|
this.renderer.setSize(container.clientWidth, container.clientHeight)
|
|||
|
|
// 可选:设置clearAlpha确保完全透明
|
|||
|
|
this.renderer.setClearAlpha(0);
|
|||
|
|
container.appendChild(this.renderer.domElement)
|
|||
|
|
|
|||
|
|
// 4. 添加OrbitControls并配置为2D模式
|
|||
|
|
this.controls = new OrbitControls(this.camera, this.renderer.domElement)
|
|||
|
|
this.configure2DControls()
|
|||
|
|
|
|||
|
|
// 5. 添加2D坐标轴辅助
|
|||
|
|
const axesHelper = new THREE.AxesHelper(10)
|
|||
|
|
this.scene.add(axesHelper)
|
|||
|
|
|
|||
|
|
// 6. 初始化点云
|
|||
|
|
this.initPointCloud()
|
|||
|
|
|
|||
|
|
// 7. 初始化车辆精灵
|
|||
|
|
this.initVehicleSprite()
|
|||
|
|
},
|
|||
|
|
|
|||
|
|
configure2DControls() {
|
|||
|
|
// 禁用旋转
|
|||
|
|
this.controls.enableRotate = false
|
|||
|
|
|
|||
|
|
// 启用平面平移
|
|||
|
|
this.controls.screenSpacePanning = true
|
|||
|
|
|
|||
|
|
// 设置缩放限制
|
|||
|
|
this.controls.minZoom = this.minZoom
|
|||
|
|
this.controls.maxZoom = this.maxZoom
|
|||
|
|
|
|||
|
|
// 启用阻尼效果
|
|||
|
|
this.controls.enableDamping = true
|
|||
|
|
this.controls.dampingFactor = 0.25
|
|||
|
|
|
|||
|
|
// 确保相机保持2D视角
|
|||
|
|
this.controls.addEventListener('change', () => {
|
|||
|
|
this.camera.position.z = 40
|
|||
|
|
// this.camera.lookAt(this.controls.target)
|
|||
|
|
})
|
|||
|
|
|
|||
|
|
// 修复触摸事件处理
|
|||
|
|
this.fixTouchEvents()
|
|||
|
|
},
|
|||
|
|
|
|||
|
|
fixTouchEvents() {
|
|||
|
|
// 确保控制器的触摸处理函数存在
|
|||
|
|
if (!this.controls) return;
|
|||
|
|
|
|||
|
|
// 备份原始触摸处理函数
|
|||
|
|
const originalOnTouchStart = this.controls.onTouchStart
|
|||
|
|
const originalOnTouchMove = this.controls.onTouchMove
|
|||
|
|
const originalOnTouchEnd = this.controls.onTouchEnd
|
|||
|
|
|
|||
|
|
// 修复触摸开始事件
|
|||
|
|
this.controls.onTouchStart = (event) => {
|
|||
|
|
if (!event.touches) return
|
|||
|
|
if (originalOnTouchStart) originalOnTouchStart(event)
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 修复触摸移动事件
|
|||
|
|
this.controls.onTouchMove = (event) => {
|
|||
|
|
if (!event.touches || event.touches.length === 0) return
|
|||
|
|
if (event.touches[0] && event.touches[0].clientX !== undefined) {
|
|||
|
|
if (originalOnTouchMove) originalOnTouchMove(event)
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 修复触摸结束事件
|
|||
|
|
this.controls.onTouchEnd = (event) => {
|
|||
|
|
if (!event.touches) return
|
|||
|
|
if (originalOnTouchEnd) originalOnTouchEnd(event)
|
|||
|
|
}
|
|||
|
|
},
|
|||
|
|
|
|||
|
|
initPointCloud() {
|
|||
|
|
const geometry = new THREE.BufferGeometry()
|
|||
|
|
const material = new THREE.PointsMaterial({
|
|||
|
|
color: 0xffffff,
|
|||
|
|
size: 1,
|
|||
|
|
transparent: true,
|
|||
|
|
opacity: 1
|
|||
|
|
})
|
|||
|
|
|
|||
|
|
this.pointCloud = new THREE.Points(geometry, material)
|
|||
|
|
this.scene.add(this.pointCloud)
|
|||
|
|
},
|
|||
|
|
|
|||
|
|
initVehicleSprite() {
|
|||
|
|
const textureLoader = new THREE.TextureLoader()
|
|||
|
|
textureLoader.load(this.vehicleImage, (texture) => {
|
|||
|
|
// 97px × 67px图片的比例
|
|||
|
|
const imageAspect = 97 / 67
|
|||
|
|
const spriteWidth = 2
|
|||
|
|
const spriteHeight = spriteWidth / imageAspect
|
|||
|
|
|
|||
|
|
this.vehicleSprite = new THREE.Sprite(
|
|||
|
|
new THREE.SpriteMaterial({
|
|||
|
|
map: texture,
|
|||
|
|
transparent: true,
|
|||
|
|
opacity: 0.9
|
|||
|
|
})
|
|||
|
|
)
|
|||
|
|
this.vehicleSprite.scale.set(spriteWidth, spriteHeight, 1)
|
|||
|
|
|
|||
|
|
// 角度偏移(根据图片初始朝向调整)
|
|||
|
|
this.updateVehiclePosition()
|
|||
|
|
this.scene.add(this.vehicleSprite)
|
|||
|
|
})
|
|||
|
|
},
|
|||
|
|
|
|||
|
|
processNewPoints(newPoints) {
|
|||
|
|
this.pointsBuffer.push(...newPoints)
|
|||
|
|
const now = performance.now()
|
|||
|
|
if (this.pointsBuffer.length >= this.bufferUpdateThreshold || now - this.lastUpdateTime > 1000) {
|
|||
|
|
this.updatePointCloudGeometry()
|
|||
|
|
this.lastUpdateTime = now
|
|||
|
|
}
|
|||
|
|
},
|
|||
|
|
|
|||
|
|
updatePointCloudGeometry() {
|
|||
|
|
if (this.pointsBuffer.length === 0) return
|
|||
|
|
|
|||
|
|
this.points = this.points.concat(this.pointsBuffer)
|
|||
|
|
this.pointsBuffer = []
|
|||
|
|
|
|||
|
|
if (this.points.length > 50000) {
|
|||
|
|
this.points = this.points.slice(-40000)
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
const positions = new Float32Array(this.points.length * 3)
|
|||
|
|
for (let i = 0; i < this.points.length; i++) {
|
|||
|
|
positions[i * 3] = this.points[i].x
|
|||
|
|
positions[i * 3 + 1] = this.points[i].y
|
|||
|
|
positions[i * 3 + 2] = 0
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
this.pointCloud.geometry.dispose()
|
|||
|
|
this.pointCloud.geometry = new THREE.BufferGeometry()
|
|||
|
|
this.pointCloud.geometry.setAttribute('position', new THREE.BufferAttribute(positions, 3))
|
|||
|
|
},
|
|||
|
|
|
|||
|
|
updateVehiclePosition() {
|
|||
|
|
if (!this.vehicleSprite) return
|
|||
|
|
|
|||
|
|
this.vehicleSprite.position.set(
|
|||
|
|
this.position.x,
|
|||
|
|
this.position.y,
|
|||
|
|
0
|
|||
|
|
)
|
|||
|
|
const radians = this.rotation * (Math.PI / 180)
|
|||
|
|
|
|||
|
|
this.vehicleSprite.rotation.X = radians
|
|||
|
|
},
|
|||
|
|
|
|||
|
|
startRendering() {
|
|||
|
|
const render = () => {
|
|||
|
|
this.controls.update()
|
|||
|
|
this.renderer.render(this.scene, this.camera)
|
|||
|
|
this.animationFrameId = requestAnimationFrame(render)
|
|||
|
|
}
|
|||
|
|
render()
|
|||
|
|
},
|
|||
|
|
|
|||
|
|
handleResize() {
|
|||
|
|
const container = this.$refs.container
|
|||
|
|
const width = container.clientWidth
|
|||
|
|
const height = container.clientHeight
|
|||
|
|
const aspect = width / height
|
|||
|
|
|
|||
|
|
this.camera.left = -this.viewSize * aspect / 2
|
|||
|
|
this.camera.right = this.viewSize * aspect / 2
|
|||
|
|
this.camera.top = this.viewSize / 2
|
|||
|
|
this.camera.bottom = -this.viewSize / 2
|
|||
|
|
this.camera.updateProjectionMatrix()
|
|||
|
|
|
|||
|
|
this.renderer.setSize(width, height)
|
|||
|
|
},
|
|||
|
|
|
|||
|
|
zoomToArea(minX, maxX, minY, maxY) {
|
|||
|
|
const width = maxX - minX
|
|||
|
|
const height = maxY - minY
|
|||
|
|
const centerX = (minX + maxX) / 2
|
|||
|
|
const centerY = (minY + maxY) / 2
|
|||
|
|
|
|||
|
|
const margin = 0.1
|
|||
|
|
const targetViewSize = Math.max(width, height) * (1 + margin)
|
|||
|
|
|
|||
|
|
// 计算合适的缩放级别
|
|||
|
|
const zoomLevel = this.viewSize / targetViewSize
|
|||
|
|
this.controls.target.set(centerX, centerY, 0)
|
|||
|
|
// 通过调整camera.zoom实现平滑缩放
|
|||
|
|
this.camera.zoom = Math.min(this.maxZoom, Math.max(this.minZoom, zoomLevel))
|
|||
|
|
this.camera.updateProjectionMatrix()
|
|||
|
|
this.controls.update()
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
</script>
|
|||
|
|
|
|||
|
|
<style lang="stylus" scoped>
|
|||
|
|
.canvas-container
|
|||
|
|
position relative
|
|||
|
|
display flex
|
|||
|
|
justify-content: center;
|
|||
|
|
align-items: center;
|
|||
|
|
height calc(100% - 1rem)
|
|||
|
|
margin .14rem 0
|
|||
|
|
box-shadow inset 1px 1px 7px 2px #4d9bcd
|
|||
|
|
overflow hidden
|
|||
|
|
.point-cloud-container
|
|||
|
|
width 100%
|
|||
|
|
height 100%
|
|||
|
|
.reload_bg {
|
|||
|
|
width: 100%;
|
|||
|
|
height: 100%;
|
|||
|
|
display: flex;
|
|||
|
|
justify-content: center;
|
|||
|
|
align-items: center;
|
|||
|
|
position: absolute;
|
|||
|
|
top: 0;
|
|||
|
|
left: 0;
|
|||
|
|
}
|
|||
|
|
</style>
|