From 40f28bf4e3c473a4cec839087386123b32660829 Mon Sep 17 00:00:00 2001 From: JINGYJ <1458671527@qq.com> Date: Wed, 2 Jul 2025 10:58:28 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E7=82=B9=E4=BA=91=E6=95=B0=E6=8D=AE?= =?UTF-8?q?=E4=BF=AE=E6=94=B9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/views/dashboard/components/PointModal.vue | 662 +++++++++++------- 1 file changed, 408 insertions(+), 254 deletions(-) diff --git a/src/views/dashboard/components/PointModal.vue b/src/views/dashboard/components/PointModal.vue index dd1122e..37cc62d 100644 --- a/src/views/dashboard/components/PointModal.vue +++ b/src/views/dashboard/components/PointModal.vue @@ -34,274 +34,430 @@ const props = withDefaults(defineProps(), { const emit = defineEmits(); -// 定义全局变量 -let scene; -let renderer; +// 状态管理 +const pcdContainer = ref(null); +const show = computed({ + get() { return props.value; }, + set(val) { emit("update:value", val); } +}); -// 新增加载状态和进度 +// Three.js核心对象 +const scene = ref(null); +const camera = ref(null); +const renderer = ref(null); +const controls = ref(null); +const pointCloud = ref(null); + +// 状态变量 +const pcdLoaded = ref(false); 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 errorMessage = ref(""); +const topView = ref(true); // 默认使用俯视图 + +// 渲染循环ID +let animationId = null; + +// 视图状态保存 +let initialCameraPosition = null; +let initialCameraRotation = null; +let initialControlsTarget = null; +let initialCameraZoom = 1; +let pointCloudCenter = new THREE.Vector3(); // 点云中心位置 +let pointCloudSize = new THREE.Vector3(); // 点云尺寸 +let normalViewPosition = null; // 普通视图相机位置 +let normalViewRotation = null; // 普通视图相机旋转 + +// 窗口大小调整处理 +const handleResize = () => { + if (camera.value && renderer.value && pcdContainer.value) { + const container = pcdContainer.value; + camera.value.aspect = container.clientWidth / container.clientHeight; + camera.value.updateProjectionMatrix(); + renderer.value.setSize(container.clientWidth, container.clientHeight); + } }; -const show = computed({ - get() { - return props.value; +// 处理点云加载和渲染 +const loadPointCloud = (url) => { + // 重置状态 + errorMessage.value = ""; + loading.value = true; + + // 清除现有点云 + if (pointCloud.value && scene.value) { + scene.value.remove(pointCloud.value); + pointCloud.value.geometry.dispose(); + pointCloud.value.material.dispose(); + pointCloud.value = null; + } + + const loader = new PCDLoader(); + loader.load( + url, + (parsedPointCloud) => { + console.log("PCD解析成功,点数:", parsedPointCloud.geometry.attributes.position.count); + processPointCloud(parsedPointCloud); }, - set(val: boolean) { - emit("update:value", val); + (xhr) => { + const percent = Math.round((xhr.loaded / xhr.total) * 100); + console.log(`加载进度: ${percent}%`); + }, + (error) => { + console.error("点云加载失败:", error); + errorMessage.value = "点云文件加载失败"; + loading.value = false; } -}); -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); - } - } +// 处理解析后的点云数据 +const processPointCloud = (parsedPointCloud) => { + // 保存点云对象 + pointCloud.value = markRaw(parsedPointCloud); + + // 计算点云边界和尺寸 + const box = new THREE.Box3().setFromObject(pointCloud.value); + box.getCenter(pointCloudCenter); + box.getSize(pointCloudSize); + + const maxDim = Math.max(pointCloudSize.x, pointCloudSize.y, pointCloudSize.z); + + // 调整点云位置使其居中 + pointCloud.value.position.sub(pointCloudCenter); + + // 应用颜色渐变 + applyColorGradient(pointCloud.value, maxDim); + + // 添加到场景 + scene.value.add(pointCloud.value); + + // 计算相机距离和位置 + const fov = camera.value.fov * (Math.PI / 180); + const distance = maxDim / 2 / Math.tan(fov / 2); + + // 保存普通视角的相机位置和旋转 + normalViewPosition = new THREE.Vector3(0, distance, 0); + normalViewRotation = new THREE.Euler(0, 0, 0); + + // 设置相机位置为俯视图 + setTopView(distance); + + // 保存初始状态 + initialCameraPosition = camera.value.position.clone(); + initialCameraRotation = camera.value.rotation.clone(); + initialCameraZoom = camera.value.zoom; + initialControlsTarget = controls.value.target.clone(); + + // 更新状态 + pcdLoaded.value = true; + loading.value = false; + + // 触发重绘 + controls.value.update(); +}; + +// 设置俯视图 +const setTopView = (distance) => { + if (!camera.value || !controls.value) return; + + // 设置相机位置为正上方 + camera.value.position.set(0, 0, distance); + // 设置相机朝向为向下 (沿Z轴负方向) + camera.value.lookAt(0, 0, 0); + + // 锁定控制器只能绕Z轴旋转 (水平旋转) + controls.value.minPolarAngle = Math.PI / 2; + controls.value.maxPolarAngle = Math.PI / 2; + + topView.value = true; +}; - animate(); +// 切换俯视图和普通视图 +const toggleTopView = () => { + if (!pcdLoaded.value) return; + + if (topView.value) { + // 切换到普通视图 + camera.value.position.copy(normalViewPosition); + camera.value.rotation.copy(normalViewRotation); + + // 解锁控制器 + controls.value.minPolarAngle = 0; + controls.value.maxPolarAngle = Math.PI; + + topView.value = false; + } else { + // 切换到俯视图 + const fov = camera.value.fov * (Math.PI / 180); + const distance = Math.max(pointCloudSize.x, pointCloudSize.y, pointCloudSize.z) / 2 / Math.tan(fov / 2); + setTopView(distance); + } + + controls.value.update(); +}; + +// 应用颜色渐变 +const applyColorGradient = (pointCloud, maxDim) => { + const geometry = pointCloud.geometry; + + // 检查点云是否已经有颜色 + if (geometry.attributes.color) { + console.log("点云已有颜色信息,使用顶点颜色"); + pointCloud.material = new THREE.PointsMaterial({ + vertexColors: true, + size: maxDim * 0.0001, // 使用与参考代码相同的点大小计算方式 + sizeAttenuation: true + }); + return; + } + + console.log("点云没有颜色信息,应用颜色渐变"); + + // 创建颜色数组 + 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 { - console.error('pcdContainer is null'); + // 从黄到红的渐变 + 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); + + // 创建材质(与参考代码一致) + pointCloud.material = new THREE.PointsMaterial({ + vertexColors: true, + size: maxDim * 0.005, + sizeAttenuation: true + }); }; -const zoomIn = () => { - if (camera) { - camera.position.multiplyScalar(0.9); - camera.updateProjectionMatrix(); - if (controls) { - controls.update(); - } +// 初始化场景 +const initScene = () => { + // 重置保存的状态 + initialCameraPosition = null; + initialCameraRotation = null; + initialControlsTarget = null; + initialCameraZoom = 1; + pointCloudCenter = new THREE.Vector3(); + pointCloudSize = new THREE.Vector3(); + + // 创建场景 + scene.value = markRaw(new THREE.Scene()); + scene.value.background = new THREE.Color(0x000000); + + // 创建相机(与参考代码一致的参数) + const container = pcdContainer.value; + camera.value = markRaw( + new THREE.PerspectiveCamera( + 75, + container.clientWidth / container.clientHeight, + 0.1, + 1000 + ) + ); + + // 创建渲染器 + renderer.value = markRaw(new THREE.WebGLRenderer({ antialias: true })); + renderer.value.setSize(container.clientWidth, container.clientHeight); + container.appendChild(renderer.value.domElement); + + // 添加轨道控制器 + controls.value = markRaw( + new OrbitControls(camera.value, renderer.value.domElement) + ); + controls.value.enableDamping = true; + controls.value.dampingFactor = 0.05; + + // 添加环境光 + const ambientLight = markRaw(new THREE.AmbientLight(0xffffff, 0.5)); + scene.value.add(ambientLight); + + // 添加方向光 + const directionalLight = markRaw(new THREE.DirectionalLight(0xffffff, 0.8)); + directionalLight.position.set(0, 0, 1); // 从上方照射 + scene.value.add(directionalLight); + + // 启动动画循环 + const animate = () => { + if (scene.value && camera.value && renderer.value && controls.value) { + controls.value.update(); + renderer.value.render(scene.value, camera.value); } + animationId = requestAnimationFrame(animate); + }; + animationId = requestAnimationFrame(animate); + + // 监听窗口大小变化 + window.addEventListener("resize", handleResize); }; -const zoomOut = () => { - if (camera) { - camera.position.multiplyScalar(1.1); - camera.updateProjectionMatrix(); - if (controls) { - controls.update(); - } +// 关闭对话框前重置视图 +const resetViewBeforeClose = () => { + if (controls.value && camera.value && initialCameraPosition && pcdLoaded.value) { + // 重置相机到初始状态 + camera.value.position.copy(initialCameraPosition); + camera.value.rotation.copy(initialCameraRotation); + camera.value.zoom = initialCameraZoom; + camera.value.updateProjectionMatrix(); + + controls.value.target.copy(initialControlsTarget); + controls.value.update(); + + // 强制渲染一帧 + if (scene.value && renderer.value) { + renderer.value.render(scene.value, camera.value); } + } }; -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(); - } +// 关闭对话框清理资源 +const handleClose = () => { + // 关闭前重置视图 + resetViewBeforeClose(); + + // 取消动画循环 + if (animationId) { + cancelAnimationFrame(animationId); + animationId = null; + } + + // 移除渲染器 + if (pcdContainer.value && renderer.value) { + pcdContainer.value.removeChild(renderer.value.domElement); + } + + // 释放资源 + if (scene.value) { + scene.value.traverse((obj) => { + if (obj instanceof THREE.Mesh || obj instanceof THREE.Points) { + obj.geometry.dispose(); + if (obj.material instanceof THREE.Material) { + obj.material.dispose(); + } else { + obj.material.forEach(m => m.dispose()); } + } + }); + } + + // 销毁控制器 + if (controls.value) { + controls.value.dispose(); + controls.value = null; + } + + // 重置状态 + scene.value = null; + camera.value = null; + renderer.value = null; + pointCloud.value = null; + pcdLoaded.value = false; + loading.value = false; + + // 移除事件监听 + window.removeEventListener("resize", handleResize); + + // 通知父组件关闭 + emit("update:value", false); +}; - // 恢复控制器目标位置 - controls.target.copy(initialControlsTarget); +// 对话框打开时初始化 +const onDialogOpened = () => { + if (!props.info.point_cloud_url) { + errorMessage.value = "没有点云数据URL"; + return; + } + + // 初始化场景 + initScene(); + + // 加载点云 + loadPointCloud(props.info.point_cloud_url); +}; - // 重置控制器的所有状态 - controls.target0.copy(initialControlsTarget); - controls.position0.copy(initialCameraPosition); - controls.zoom0 = initialCameraZoom; +// 相机控制函数 +const zoomIn = () => { + if (camera.value) { + camera.value.position.multiplyScalar(0.9); // 与参考代码相同的缩放因子 + camera.value.updateProjectionMatrix(); + controls.value.update(); + } +}; - // 延迟更新控制器 - setTimeout(() => { - if (controls) { - controls.update(); - } - }, 100); - } else { - console.error("Initial state is not defined"); - } +const zoomOut = () => { + if (camera.value) { + camera.value.position.multiplyScalar(1.1); // 与参考代码相同的缩放因子 + camera.value.updateProjectionMatrix(); + controls.value.update(); + } }; + +// 回到中心功能(简化版本,直接设置而不是动画) +const returnToCenter = () => { + if (initialCameraPosition && initialCameraRotation && initialControlsTarget) { + camera.value.position.copy(initialCameraPosition); + camera.value.rotation.copy(initialCameraRotation); + camera.value.zoom = initialCameraZoom; + camera.value.updateProjectionMatrix(); + + controls.value.target.copy(initialControlsTarget); + controls.value.update(); + } +}; + +// 组件卸载时清理 +onUnmounted(() => { + // 确保动画循环已取消 + if (animationId) { + cancelAnimationFrame(animationId); + animationId = null; + } + + window.removeEventListener("resize", handleResize); +});