You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

372 lines
12 KiB
Vue

This file contains ambiguous Unicode characters!

This file contains ambiguous Unicode characters that may be confused with others in your current locale. If your use case is intentional and legitimate, you can safely ignore this warning. Use the Escape button to highlight these characters.

<template>
<el-dialog class="pointModal-wrap" v-model="show" @close="handleClose" @opened="onDialogOpened">
<div class="pcd-container">
<div ref="pcdContainer" style="width: 100%; height: 100%" />
<span class="button-first" @click="zoomIn"></span>
<span class="button-second" @click="zoomOut"></span>
<span class="button-thrid" :disabled="!pcdLoaded" @click="returnToCenter"></span>
<!-- 新增进度条 -->
<div v-if="loading" class="progress-bar">
<div class="progress" :style="{ width: progress + '%' }"></div>
</div>
</div>
</el-dialog>
</template>
<script lang="ts" setup>
import { ref, computed, defineProps, defineEmits, withDefaults } from 'vue';
import * as THREE from "three";
import { PCDLoader } from "three/examples/jsm/loaders/PCDLoader.js";
import { OrbitControls } from "three/examples/jsm/controls/OrbitControls.js";
interface Props {
/** 弹窗显隐 */
value: boolean;
info: Record<string, any>;
}
interface Emits {
(e: "update:value", val: boolean): void;
}
const props = withDefaults(defineProps<Props>(), {
value: false,
info: {}
});
const emit = defineEmits<Emits>();
// 定义全局变量
let scene;
let renderer;
// 新增加载状态和进度
const loading = ref(false);
const progress = ref(0);
// 处理对话框关闭事件
const handleClose = () => {
if (pcdContainer.value) {
// 移除渲染器的 DOM 元素
while (pcdContainer.value.firstChild) {
pcdContainer.value.removeChild(pcdContainer.value.firstChild);
}
// 释放场景资源
if (scene) {
scene.traverse((object) => {
if (object instanceof THREE.Mesh) {
object.geometry.dispose();
if (object.material instanceof THREE.Material) {
object.material.dispose();
} else {
object.material.forEach((material) => material.dispose());
}
}
});
}
// 销毁控制器
if (controls) {
controls.dispose();
}
// 移除窗口大小变化监听器
// window.removeEventListener('resize', onWindowResize);
// 重置变量
scene = null;
camera = null;
renderer = null;
controls = null;
pcdLoaded.value = false;
}
emit("update:value", false);
};
const show = computed({
get() {
return props.value;
},
set(val: boolean) {
emit("update:value", val);
}
});
const pcdContainer = ref(null);
let initialCameraPosition;
let initialCameraRotation;
let initialControlsTarget;
let initialCameraZoom;
let controls;
let camera;
const pcdLoaded = ref(false);
const onDialogOpened = () => {
// 检查点云数据URL是否存在
if (!props.info.point_cloud_url) {
return;
}
if (pcdContainer.value) {
scene = new THREE.Scene();
camera = new THREE.PerspectiveCamera(
75,
pcdContainer.value.clientWidth / pcdContainer.value.clientHeight,
0.1,
1000
);
renderer = new THREE.WebGLRenderer();
renderer.setSize(pcdContainer.value.clientWidth, pcdContainer.value.clientHeight);
pcdContainer.value.appendChild(renderer.domElement);
// 创建 OrbitControls 实例
controls = new OrbitControls(camera, renderer.domElement);
const loader = new PCDLoader();
// 开始加载,设置加载状态为 true
loading.value = true;
loader.load(
props.info.point_cloud_url,
function (pointCloud) {
// 加载完成,设置加载状态为 false
loading.value = false;
// 调整点云的位置到原点
pointCloud.position.set(0, 100, 0);
// 获取点云的几何体
const geometry = pointCloud.geometry;
// 创建颜色数组
const colors = [];
const positions = geometry.attributes.position.array;
const numPoints = positions.length / 3;
// 定义起始颜色和结束颜色
const startColor = new THREE.Color(0x0000ff); // 蓝色
const midColor1 = new THREE.Color(0x00ff00); // 绿色
const midColor2 = new THREE.Color(0xffff00); // 黄色
const endColor = new THREE.Color(0xff0000); // 红色
// 计算点云在 z 轴上的最小值和最大值
let minZ = Infinity;
let maxZ = -Infinity;
for (let i = 0; i < numPoints; i++) {
const z = positions[i * 3 + 2];
if (z < minZ) minZ = z;
if (z > maxZ) maxZ = z;
}
for (let i = 0; i < numPoints; i++) {
const z = positions[i * 3 + 2];
// 计算颜色渐变因子
const factor = (z - minZ) / (maxZ - minZ);
let color;
if (factor < 0.33) {
// 从蓝到绿的渐变
const subFactor = factor / 0.33;
color = startColor.clone().lerp(midColor1, subFactor);
} else if (factor < 0.66) {
// 从绿到黄的渐变
const subFactor = (factor - 0.33) / 0.33;
color = midColor1.clone().lerp(midColor2, subFactor);
} else {
// 从黄到红的渐变
const subFactor = (factor - 0.66) / 0.34;
color = midColor2.clone().lerp(endColor, subFactor);
}
// 将颜色添加到颜色数组中
colors.push(color.r, color.g, color.b);
}
// 创建颜色属性
const colorAttribute = new THREE.Float32BufferAttribute(colors, 3);
geometry.setAttribute('color', colorAttribute);
// 创建一个新的材质并启用顶点颜色
const material = new THREE.PointsMaterial({ vertexColors: true });
pointCloud.material = material;
// 调整点云的缩放比例
pointCloud.scale.set(1, 1, 1);
// 计算点云的边界框
const box = new THREE.Box3().setFromObject(pointCloud);
const size = box.getSize(new THREE.Vector3());
const maxSize = Math.max(size.x, size.y, size.z);
const fov = camera.fov * (Math.PI / -180);
const distance = maxSize / 2 / Math.tan(fov / 3);
// 调整相机位置为正视图
camera.position.set(0, distance, 0);
camera.rotation.set(0, 0, 0);
camera.lookAt(0, 0, 0);
// 调整点云的旋转角度,绕 y 轴旋转 45 度Math.PI / 4 弧度)
pointCloud.rotation.set(0, 0, 0);
// 保存初始相机位置、旋转、缩放和控制器目标位置
initialCameraPosition = camera.position.clone();
initialCameraRotation = camera.rotation.clone();
initialCameraZoom = camera.zoom;
initialControlsTarget = controls.target.clone();
scene.add(pointCloud);
pcdLoaded.value = true;
// // 监听窗口大小变化
// const onWindowResize = () => {
// camera.aspect = pcdContainer.value.clientWidth / pcdContainer.value.clientHeight;
// camera.updateProjectionMatrix();
// renderer.setSize(pcdContainer.value.clientWidth, pcdContainer.value.clientHeight);
// };
// window.addEventListener('resize', onWindowResize);
},
function (xhr) {
// 更新加载进度
progress.value = (xhr.loaded / xhr.total) * 100;
},
function (error) {
// 加载失败,设置加载状态为 false
loading.value = false;
console.error('点云文件加载失败:', error);
}
);
function animate() {
requestAnimationFrame(animate);
// 更新控制器
if (controls) {
controls.update();
}
if (renderer && scene && camera) {
renderer.render(scene, camera);
}
}
animate();
} else {
console.error('pcdContainer is null');
}
};
const zoomIn = () => {
if (camera) {
camera.position.multiplyScalar(0.9);
camera.updateProjectionMatrix();
if (controls) {
controls.update();
}
}
};
const zoomOut = () => {
if (camera) {
camera.position.multiplyScalar(1.1);
camera.updateProjectionMatrix();
if (controls) {
controls.update();
}
}
};
const returnToCenter = () => {
if (initialCameraPosition && initialCameraRotation && initialControlsTarget && camera && controls) {
// 重置控制器的临时状态
controls.reset();
// 恢复相机位置
camera.position.copy(initialCameraPosition);
// 恢复相机旋转角度
camera.rotation.copy(initialCameraRotation);
// 恢复相机缩放
camera.zoom = initialCameraZoom;
// 多次更新投影矩阵和控制器
for (let i = 0; i < 3; i++) {
camera.updateProjectionMatrix();
if (controls) {
controls.update();
}
}
// 恢复控制器目标位置
controls.target.copy(initialControlsTarget);
// 重置控制器的所有状态
controls.target0.copy(initialControlsTarget);
controls.position0.copy(initialCameraPosition);
controls.zoom0 = initialCameraZoom;
// 延迟更新控制器
setTimeout(() => {
if (controls) {
controls.update();
}
}, 100);
} else {
console.error("Initial state is not defined");
}
};
</script>
<style lang="scss">
.pointModal-wrap.el-dialog {
box-sizing: border-box;
width: 1600px;
height: 810px;
background-image: url("@/assets/common/pointModalBg.png");
background-size: 100% 100%;
background-position: center;
background-repeat: no-repeat;
background-color: #000;
.pcd-container {
position: relative;
width: 1560px;
height: 760px;
.button-first,
.button-second,
.button-thrid {
position: absolute;
width: 24px;
height: 24px;
cursor: pointer;
background-image: url("@/assets/common/amplify_btn.png");
background-size: 100% 100%;
background-position: center;
background-repeat: no-repeat;
bottom: 112px;
right: 32px;
/* */
transition: all 0.3s ease;
}
.button-second {
bottom: 72px;
right: 32px;
background-image: url("@/assets/common/reduce_btn.png");
background-size: 100% 100%;
background-position: center;
background-repeat: no-repeat;
/* */
transition: all 0.3s ease;
}
.button-thrid {
bottom: 33px;
right: 32px;
background-image: url("@/assets/common/reset_btn.png");
background-size: 100% 100%;
background-position: center;
background-repeat: no-repeat;
/* */
transition: all 0.3s ease;
}
.progress-bar {
position: absolute;
bottom: 20px;
left: 50%;
transform: translateX(-50%);
width: 80%;
height: 20px;
background-color: #ccc;
border-radius: 10px;
overflow: hidden;
}
.progress {
height: 100%;
background-color: #007bff;
transition: width 0.3s ease;
}
}
}
</style>