|
|
<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> |