Files
apt15e/src/pages/modules/gl-map-1 copy.vue
2025-08-27 11:16:49 +08:00

576 lines
20 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<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'
// import { points1 } from '../../config/point1.js'
// import { points2 } from '../../config/point2.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, // 限制更新频率,毫秒
// 点云边界:记录所有点的最大/最小x、y
pointBounds: {
minX: Infinity, // 初始设为无穷大,方便后续比较
maxX: -Infinity,
minY: Infinity,
maxY: -Infinity
},
boundMargin: 0.1, // 边界边距避免点贴屏幕边缘值为10%的范围)
defaultViewSize: 20, // 无点云时的默认可视范围
// 标记组件是否已销毁
isDestroyed: false,
minZoomRatio: 0.5,
maxZoomRatio: 3,
baseViewSize: 0
}
},
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 = 1;
this.controls.panSpeed = 1;
this.controls.touches = {
ONE: THREE.TOUCH.PAN, // 单指拖动为平移
TWO: THREE.TOUCH.DOLLY_PAN // 双指拖动为缩放和平移
};
// 添加错误处理逻辑
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.5, // 初始值会被动态覆盖
sizeAttenuation: true, // 关键!
transparent: true,
opacity: 1
});
// 创建点云网格
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 material = new THREE.SpriteMaterial({
map: texture,
transparent: true, // 保留图片透明区域
opacity: 1,
depthWrite: false // 避免被点云遮挡(可选,根据层级需求调整)
});
this.carMesh = new THREE.Sprite(material);
this.scene.add(this.carMesh);
// 初始计算小车尺寸
this.calculateCarSize();
// 初始位置角度更新
if (this.carPosition) {
this.updateCarPosition(this.carPosition);
}
}, undefined, (error) => {
console.error('小车图片加载失败:', error);
});
},
/**
* 单独的小车尺寸计算方法,确保窗口缩放时可复用
*/
calculateCarSize() {
if (!this.carMesh) return;
const container = this.$refs.canvasContainer;
const canvasWidth = container.clientWidth;
const canvasHeight = container.clientHeight;
const aspect = canvasWidth / canvasHeight;
// 核心计算将50px转换为Three.js世界坐标
// 公式:(目标像素尺寸 / 画布尺寸) * 相机可视范围 * 校正系数
const spriteSizeX = (50 / canvasWidth) * this.viewSize * aspect;
const spriteSizeY = (50 / canvasHeight) * this.viewSize;
this.carMesh.scale.set(spriteSizeX, spriteSizeY, 1);
},
/**
* 更新小车位置和角度
*/
updateCarPosition(position) {
if (this.carMesh && position) {
this.carMesh.position.x = position.x * this.pointScale;
this.carMesh.position.y = position.y * this.pointScale;
this.carMesh.position.z = 1;
// 转换角度为弧度并调整方向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 () {
this.initWebSocket();
// const pointData = points.data
// this.updatePointCloud(pointData);
// setTimeout(() => {
// const arr = points1.data
// this.updatePointCloud(arr);
// }, 1000)
// setTimeout(() => {
// const arr = points2.data
// 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; // 更新总点数
// 重新计算所有点云的边界
this.pointBounds = { minX: Infinity, maxX: -Infinity, minY: Infinity, maxY: -Infinity };
this.allPoints.forEach(point => {
const scaledX = (point.x || 0) * this.pointScale; // 缩放后的实际坐标
const scaledY = (point.y || 0) * this.pointScale;
// 更新最大/最小值
this.pointBounds.minX = Math.min(this.pointBounds.minX, scaledX);
this.pointBounds.maxX = Math.max(this.pointBounds.maxX, scaledX);
this.pointBounds.minY = Math.min(this.pointBounds.minY, scaledY);
this.pointBounds.maxY = Math.max(this.pointBounds.maxY, scaledY);
});
this.adjustCameraByBounds();
// 动态扩展缓冲区
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;
},
/**
* 根据边界动态调整相机可视范围
*/
adjustCameraByBounds() {
const container = this.$refs.canvasContainer;
const aspect = container.clientWidth / container.clientHeight; // 画布宽高比
// 1. 计算点云的实际范围(宽和高)
const pointRangeX = this.pointBounds.maxX - this.pointBounds.minX;
const pointRangeY = this.pointBounds.maxY - this.pointBounds.minY;
// 2. 计算“基准可视范围”(刚好覆盖点云+边距的最小范围)
let baseViewSize = this.defaultViewSize; // 默认值(无点云时)
if (this.allPoints.length > 0 && !(pointRangeX === 0 && pointRangeY === 0)) {
const requiredViewSizeX = pointRangeX * (1 + this.boundMargin); // X方向需覆盖范围
const requiredViewSizeY = pointRangeY * (1 + this.boundMargin); // Y方向需覆盖范围
// 基准可视范围取X/Y方向中较大值确保适配宽高比后覆盖所有点
baseViewSize = Math.max(requiredViewSizeX / aspect, requiredViewSizeY);
}
this.baseViewSize = baseViewSize; // 缓存基准值,用于后续缩放限制
// 3. 更新相机可视范围(基于基准值)
this.viewSize = baseViewSize; // 初始可视范围=基准范围
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(); // 必须更新投影矩阵,否则不生效
// 4. 让相机焦点对准点云中心(可选,确保点云在屏幕中间)
const centerX = (this.pointBounds.minX + this.pointBounds.maxX) / 2;
const centerY = (this.pointBounds.minY + this.pointBounds.maxY) / 2;
this.controls.target.set(centerX, centerY, 0); // 控制器目标对准点云中心
this.camera.position.set(centerX, centerY, 100); // 相机位置也对准中心Z轴不变
// 5. 关键设置OrbitControls缩放限制正交相机专用
if (this.controls) {
// 5.1 正交相机缩放限制minZoom/maxZoom基于基准可视范围的倍数
// 逻辑viewSize = baseViewSize / zoomRatio → 缩放倍数越大viewSize越小画面放大
this.controls.minZoom = this.minZoomRatio; // 最小缩放倍数→最大viewSize缩小画面
this.controls.maxZoom = this.maxZoomRatio; // 最大缩放倍数→最小viewSize放大画面
// 5.2 兼容控制器逻辑设置minDistance/maxDistance虽正交相机不用但避免异常
this.controls.minDistance = 50; // 相机最小Z轴距离小于此值可能穿模
this.controls.maxDistance = 200; // 相机最大Z轴距离大于此值画面过小
// 5.3 锁定缩放中心(可选:确保缩放时以点云中心为基准)
this.controls.zoomToCursor = false; // 关闭“鼠标位置缩放”,避免缩放偏移
}
// 相机参数更新后,同步更新小车尺寸
this.calculateCarSize();
},
/**
* 处理窗口大小变化
*/
handleResize() {
const container = this.$refs.canvasContainer;
const width = container.clientWidth;
const height = container.clientHeight;
// 原有渲染器尺寸更新
this.renderer.setSize(width, height);
// 基于当前点云边界重新适配相机(关键)
this.adjustCameraByBounds();
// 窗口缩放时重新计算小车尺寸
this.calculateCarSize();
},
/**
* 启动动画循环
*/
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 100%
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>