This commit is contained in:
2025-08-08 17:44:36 +08:00
parent 7367906a45
commit af93101ff2
23 changed files with 926 additions and 1650 deletions

View File

@@ -0,0 +1,492 @@
<template>
<div class="point-cloud-map">
<div ref="canvasContainer" class="canvas-container"></div>
<!-- 加载状态指示器 -->
<div v-if="isLoading" class="loading-indicator">
<i class="fa fa-circle-o-notch fa-spin"></i> 加载中...
</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 {
// Three.js核心对象
scene: null,
camera: null,
renderer: null,
controls: null,
// 点云相关
pointCloudGeometry: null,
pointCloudMaterial: null,
pointCloudMesh: null,
allPoints: [], // 存储所有累加的点云数据
pointCount: 0,
pointScale: 1.0,
// 小车相关
vehicleImage: require('../../images/new/agv.png'),
carMesh: null,
carTexture: null,
// WebSocket相关
socket: null,
isLoading: true,
reconnectTimer: null, // 重连计时器
// 动画与性能
viewSize: 20,
animationId: null,
lastUpdateTime: 0,
updateInterval: 800, // 限制更新频率,毫秒
// 标记组件是否已销毁
isDestroyed: false
}
},
computed: {
...mapGetters(['serverUrl', , 'userRole', 'carPosition']),
},
watch: {
carPosition: {
handler(newVal) {
this.updateCarPosition(newVal)
},
deep: true
}
},
mounted () {
// 初始化Three.js场景
this.initThreeJs();
// 初始化控制器(拖动和缩放)
this.initControls();
// 初始化点云
this.initPointCloud();
// 初始化小车模型
this.initCar();
// 启动动画循环
this.startAnimationLoop();
// 监听窗口大小变化
window.addEventListener('resize', this.handleResize);
// 初始加载完成
setTimeout(() => {
this.isLoading = false;
}, 1000);
},
beforeDestroy () {
// 先标记组件已销毁,阻止新消息处理
this.isDestroyed = true;
// 1. 清理WebSocket先移除事件监听再关闭连接
this.closeWebSocket()
// 2. 停止动画循环并清理
if (this.animationId) {
cancelAnimationFrame(this.animationId);
this.animationId = null;
}
// 3. 清理OrbitControls关键控制器有内部事件监听
if (this.controls) {
this.controls.dispose(); // 控制器自带的清理方法,移除事件监听
this.controls = null;
}
// 4. 清理Three.js场景对象先从场景移除再释放资源
if (this.scene) {
// 移除点云网格
if (this.pointCloudMesh) {
this.scene.remove(this.pointCloudMesh);
}
// 移除小车模型
if (this.carMesh) {
this.scene.remove(this.carMesh);
}
// 清空场景(彻底释放所有子对象引用)
this.scene.clear();
this.scene = null;
}
// 5. 释放Three.js渲染器及DOM
if (this.renderer) {
// 从DOM中移除canvas元素避免残留DOM节点
const container = this.$refs.canvasContainer;
if (container && this.renderer.domElement) {
container.removeChild(this.renderer.domElement);
}
// 释放渲染器资源
this.renderer.dispose();
this.renderer.forceContextLoss(); // 强制释放WebGL上下文关键
this.renderer = null;
}
// 6. 释放几何体和材质(确保先移除引用)
if (this.pointCloudGeometry) {
this.pointCloudGeometry.dispose();
this.pointCloudGeometry = null;
}
if (this.pointCloudMaterial) {
// 先清除材质的纹理引用(避免纹理无法释放)
this.pointCloudMaterial.map = null;
this.pointCloudMaterial.dispose();
this.pointCloudMaterial = null;
}
// 7. 释放小车纹理(确保材质不再引用)
if (this.carTexture) {
this.carTexture.dispose();
this.carTexture = null;
}
// 8. 移除窗口大小变化监听(避免重复监听)
window.removeEventListener('resize', this.handleResize);
// 9. 清空其他引用帮助GC回收
this.camera = null;
this.carMesh = null;
this.pointCloudMesh = null;
},
methods: {
/**
* 初始化Three.js场景、相机和渲染器
*/
initThreeJs() {
const container = this.$refs.canvasContainer;
const aspect = container.clientWidth / container.clientHeight
// 创建场景
this.scene = new THREE.Scene();
// 创建正交相机适合2D场景
this.camera = new THREE.OrthographicCamera(
-this.viewSize * aspect / 2, // left
this.viewSize * aspect / 2, // right
this.viewSize / 2, // top
-this.viewSize / 2, // bottom
0.1, // near
1000 // far
);
this.camera.position.z = 100; // 保持在Z轴上方不影响2D视图
// 创建渲染器 - 启用alpha通道实现透明背景
this.renderer = new THREE.WebGLRenderer({
antialias: true,
alpha: true, // 关键启用alpha通道
transparent: true // 关键:允许透明
});
this.renderer.setSize(container.clientWidth, container.clientHeight);
this.renderer.setPixelRatio(window.devicePixelRatio);
// 可选设置clearAlpha确保完全透明
this.renderer.setClearAlpha(0);
container.appendChild(this.renderer.domElement);
},
/**
* 初始化控制器,支持拖动和缩放
*/
initControls() {
this.controls = new OrbitControls(this.camera, this.renderer.domElement);
this.controls.enableRotate = false; // 禁用旋转保持2D视图
this.controls.enableZoom = true; // 启用缩放
this.controls.enablePan = true; // 启用平移(拖动)
this.controls.screenSpacePanning = true; // 2D平移模式
this.controls.touchZoomSpeed = 0.5; // 降低触摸缩放速度
this.controls.panSpeed = 0.5; // 降低平移速度
// 鼠标/触摸优化
this.controls.mouseButtons = {
LEFT: THREE.MOUSE.PAN, // 左键拖动
MIDDLE: THREE.MOUSE.DOLLY // 中键缩放(可选)
};
// 触摸支持
this.controls.touchAction = 'none';
this.controls.touchPan = true;
this.controls.touchZoom = true;
// 添加错误处理逻辑
this.controls.addEventListener('error', (event) => {
console.error('OrbitControls error:', event);
});
},
/**
* 初始化点云
*/
initPointCloud() {
// 使用BufferGeometry提高大量点的渲染性能
this.pointCloudGeometry = new THREE.BufferGeometry();
// 点材质设置
this.pointCloudMaterial = new THREE.PointsMaterial({
color: 0xFFFFFF,
size: 1, // 初始值会被动态覆盖
sizeAttenuation: true, // 关键!
transparent: true,
opacity: 0.8
});
// 创建点云网格
this.pointCloudMesh = new THREE.Points(
this.pointCloudGeometry,
this.pointCloudMaterial
);
this.scene.add(this.pointCloudMesh);
// 初始化历史数据为空
this.allPoints = [];
},
/**
* 初始化小车模型
*/
initCar() {
// 加载小车图片作为精灵
const loader = new THREE.TextureLoader();
this.carTexture = loader.load(this.vehicleImage, (texture) => {
const imageAspect = 109 / 109
const spriteWidth = 2
const spriteHeight = spriteWidth / imageAspect
// 创建平面几何体
const geometry = new THREE.PlaneGeometry(spriteWidth, spriteHeight);
// 创建材质并应用纹理
const material = new THREE.MeshBasicMaterial({
map: texture,
transparent: true,
opacity: 0.9
});
// 创建网格对象
this.carMesh = new THREE.Mesh(geometry, material);
// 设置锚点为小车中心(以便旋转正确)
this.carMesh.position.set(spriteWidth / 2, spriteHeight / 2, 0);
// 添加到场景中
this.scene.add(this.carMesh);
// 初始位置角度更新
if (this.carPosition) {
this.updateCarPosition(this.carPosition);
}
}, undefined, (error) => {
console.error('小车图片加载失败:', error);
});
},
/**
* 更新小车位置和角度
*/
updateCarPosition(position) {
if (this.carMesh && position) {
this.carMesh.position.x = position.x * this.pointScale;
this.carMesh.position.y = position.y * this.pointScale;
// 转换角度为弧度并调整方向Three.js使用弧度
this.carMesh.rotation.z = position.angle;
}
},
/**
* 初始化WebSocket连接
*/
initWebSocket() {
if (this.reconnectTimer) clearTimeout(this.reconnectTimer)
const wsHost = this.serverUrl.replace(/^https?:\/\//, '')
this.socket = new WebSocket(`ws://${wsHost}/webSocket/PointCloudData/${this.userRole}`);
this.socket.onopen = () => {
console.log('WebSocket连接已建立');
this.isLoading = false;
};
this.socket.onmessage = (event) => {
// 组件已销毁则直接返回,不处理消息
if (this.isDestroyed) return
try {
// 限制更新频率,优化性能
const now = Date.now();
if (now - this.lastUpdateTime < this.updateInterval) {
return;
}
this.lastUpdateTime = now;
if (this.reconnectTimer) clearTimeout(this.reconnectTimer)
const pointData = JSON.parse(event.data);
this.updatePointCloud(pointData.data);
} catch (error) {
console.error('解析点云数据失败:', error);
}
};
this.socket.onclose = (event) => {
console.log('WebSocket连接已关闭代码:', event.code);
this.isLoading = true;
// 自动重连
this.reconnectTimer = setTimeout(() => this.initWebSocket(), 3000);
};
this.socket.onerror = (error) => {
if (this.reconnectTimer) clearTimeout(this.reconnectTimer)
console.error('WebSocket错误:', error);
this.isLoading = true;
};
},
closeWebSocket () {
if (this.reconnectTimer) clearTimeout(this.reconnectTimer)
if (this.socket) {
this.socket.onopen = null;
this.socket.onmessage = null;
this.socket.onclose = null;
this.socket.onerror = null;
this.socket.close(1000, '组件销毁');
this.socket = null;
}
this.allPoints = []
},
init () {
// 初始化WebSocket连接
this.initWebSocket();
// const pointData = points.data
// this.updatePointCloud(pointData);
// setTimeout(() => {
// const arr = [{x: 2, y: 2}, {x: 4, y: 4}]
// this.updatePointCloud(arr);
// }, 2000)
},
/**
* 更新点云数据,优化大量点的渲染性能
*/
updatePointCloud(points) {
if (!Array.isArray(points) || points.length === 0) return;
// 用于跟踪已存在的点,使用"x,y"作为唯一标识
const existingPoints = {};
// 先将已有点添加到跟踪对象
this.allPoints.forEach(point => {
const key = `${point.x},${point.y}`;
existingPoints[key] = true;
});
// 过滤新点中的重复点
const newUniquePoints = points.filter(point => {
const key = `${point.x},${point.y}`;
if (!existingPoints[key]) {
existingPoints[key] = true;
return true;
}
return false;
});
this.allPoints = [...this.allPoints, ...newUniquePoints];
this.pointCount = this.allPoints.length; // 更新总点数
// 动态扩展缓冲区
const positionAttribute = this.pointCloudGeometry.getAttribute('position');
let positions;
if (!positionAttribute) {
// 首次初始化:创建缓冲区
positions = new Float32Array(this.pointCount * 3);
this.pointCloudGeometry.setAttribute(
'position',
new THREE.BufferAttribute(positions, 3)
);
} else {
// 后续更新:扩展现有缓冲区(避免重建)
positions = positionAttribute.array;
const newLength = this.pointCount * 3;
if (newLength > positions.length) {
// 扩展时多预留20%空间,减少频繁扩展
const newPositions = new Float32Array(Math.ceil(newLength * 1.2));
newPositions.set(positions); // 复制原有数据
positions = newPositions;
this.pointCloudGeometry.setAttribute(
'position',
new THREE.BufferAttribute(positions, 3)
);
}
}
// 填充新数据(仅更新新增部分,减少重复计算)
const startIndex = (this.pointCount - newUniquePoints.length) * 3; // 从新增点开始
for (let i = 0; i < newUniquePoints.length; i++) {
const point = newUniquePoints[i];
const index = startIndex + i * 3;
positions[index] = (point.x || 0) * this.pointScale;
positions[index + 1] = (point.y || 0) * this.pointScale;
positions[index + 2] = 0;
}
// 标记更新
this.pointCloudGeometry.getAttribute('position').needsUpdate = true;
},
/**
* 处理窗口大小变化
*/
handleResize() {
const container = this.$refs.canvasContainer;
const width = container.clientWidth;
const height = container.clientHeight;
// 更新相机
this.camera.left = width / -2;
this.camera.right = width / 2;
this.camera.top = height / 2;
this.camera.bottom = height / -2;
this.camera.updateProjectionMatrix();
// 更新渲染器
this.renderer.setSize(width, height);
},
/**
* 启动动画循环
*/
startAnimationLoop() {
const animate = () => {
this.animationId = requestAnimationFrame(animate);
// 更新控制器(用于阻尼效果)
if (this.controls) {
this.controls.update();
}
// 渲染场景
this.renderer.render(this.scene, this.camera);
};
animate();
}
}
}
</script>
<style lang="stylus" scoped>
.point-cloud-map
position relative
width 100%
height calc(100% - 1rem - 6px)
margin .14rem 0
background-color rgba(4, 33, 58, 70%)
box-shadow inset 1px 1px 7px 2px #4d9bcd
overflow hidden
.canvas-container {
width: 100%;
height: 100%;
position: relative;
z-index: 0;
}
.loading-indicator {
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
background-color: rgba(255, 255, 255, 0.8);
padding: .01rem .02rem;
border-radius: 4px;
box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1);
font-size: .12rem;
color: #333;
z-index: 100;
display: flex;
align-items: center;
}
</style>