Browse Source

feat(雷达点云): 改进点云图例样式并添加房间尺寸配置功能

- 将离散图例改为渐变条样式并添加数值标注
- 添加房间尺寸配置面板,支持动态调整房间长宽高
- 优化点云颜色计算,使用动态归一化强度值
- 改进UI样式,包括按钮交互和配置面板布局
liujia 1 tháng trước cách đây
mục cha
commit
cfc4169
1 tập tin đã thay đổi với 273 bổ sung90 xóa
  1. 273 90
      src/components/radarPointCloud/index.vue

+ 273 - 90
src/components/radarPointCloud/index.vue

@@ -10,38 +10,85 @@
 
       <!-- 信息面板 -->
       <div class="overlay">
-        <h3>雷达点云图例</h3>
-        <div class="legend-item">
-          <div class="legend-color" style="background-color: #ff0000"></div>
-          <span>强度(1800-2000)</span>
-        </div>
-        <div class="legend-item">
-          <div class="legend-color" style="background-color: #ffff00"></div>
-          <span>强度(1600-1800)</span>
-        </div>
-        <div class="legend-item">
-          <div class="legend-color" style="background-color: #00ff00"></div>
-          <span>强度(1400-1600)</span>
+        <!-- 强度颜色图例 -->
+        <div class="legend-title">强度图例</div>
+        <div class="legend-content">
+          <!-- 渐变条 -->
+          <div class="legend-gradient"> </div>
+          <!-- 数值标注 -->
+          <div class="legend-numbers">
+            <span>0.0</span>
+            <span>0.25</span>
+            <span>0.5</span>
+            <span>0.75</span>
+            <span>1.0</span>
+          </div>
         </div>
+
+        <!-- 点云数量 -->
         <div class="legend-item">
-          <div class="legend-color" style="background-color: #00ffff"></div>
-          <span>强度(1200-1400)</span>
+          <span class="legend-item-title">点云数量: </span>
+          <span class="legend-item-value" id="pointCount">{{ pointCount }}</span>
         </div>
+
+        <!-- 更新时间 -->
         <div class="legend-item">
-          <div class="legend-color" style="background-color: #0000ff"></div>
-          <span>强度(1000-1200)</span>
+          <span class="legend-item-title">更新时间: </span>
+          <span class="legend-item-value" id="updateTime">{{ lastUpdate }}</span>
         </div>
-        <p
-          >当前点数量: <span>{{ pointCount }}</span></p
-        >
-        <p
-          >最后更新: <span>{{ lastUpdate }}</span></p
-        >
+
+        <!-- 按钮区域 -->
         <a-space class="control-space" :size="12">
           <ZoomInOutlined @click="zoomIn" :disabled="isLoading" title="放大" />
           <RedoOutlined @click="resetView" :disabled="isLoading" title="重置" />
           <ZoomOutOutlined @click="zoomOut" :disabled="isLoading" title="缩小" />
+          <CodeSandboxOutlined
+            @click="configPanelVisible = !configPanelVisible"
+            :disabled="isLoading"
+            :style="{ color: configPanelVisible ? '#1677ff' : '#fff' }"
+            title="调整房间尺寸"
+          />
         </a-space>
+
+        <!-- 配置面板 -->
+        <div v-if="configPanelVisible" class="config-panel">
+          <div class="config-item">
+            <div class="config-item-label">房间宽度cm:</div>
+            <a-input-number
+              class="config-item-input"
+              v-model:value="roomDimensions.width"
+              size="small"
+              min="1"
+              :controls="false"
+              @keyup.enter="applyRoomDimensions"
+              @blur="applyRoomDimensions"
+            />
+          </div>
+          <div class="config-item">
+            <div class="config-item-label">房间长度cm:</div>
+            <a-input-number
+              class="config-item-input"
+              v-model:value="roomDimensions.length"
+              size="small"
+              min="1"
+              :controls="false"
+              @keyup.enter="applyRoomDimensions"
+              @blur="applyRoomDimensions"
+            />
+          </div>
+          <div class="config-item">
+            <div class="config-item-label">房间高度cm:</div>
+            <a-input-number
+              class="config-item-input"
+              v-model:value="roomDimensions.height"
+              size="small"
+              min="1"
+              :controls="false"
+              @keyup.enter="applyRoomDimensions"
+              @blur="applyRoomDimensions"
+            />
+          </div>
+        </div>
       </div>
     </div>
   </div>
@@ -58,7 +105,12 @@ import { RenderPass } from 'three/examples/jsm/postprocessing/RenderPass.js'
 import { ShaderPass } from 'three/examples/jsm/postprocessing/ShaderPass.js'
 import { BloomPass } from 'three/examples/jsm/postprocessing/BloomPass.js'
 import { CopyShader } from 'three/examples/jsm/shaders/CopyShader.js'
-import { ZoomInOutlined, ZoomOutOutlined, RedoOutlined } from '@ant-design/icons-vue'
+import {
+  ZoomInOutlined,
+  ZoomOutOutlined,
+  RedoOutlined,
+  CodeSandboxOutlined,
+} from '@ant-design/icons-vue'
 
 // 定义组件名称
 defineOptions({
@@ -100,7 +152,7 @@ const state = reactive({
   isLoading: true, // 加载状态
   loadingText: '正在加载雷达点云数据...', // 加载提示文本
   pointCount: 0, // 点数量
-  lastUpdate: '-', // 最后更新时间
+  lastUpdate: '--:--:--', // 最后更新时间
 })
 
 // 将响应式状态转换为ref以便在模板中使用
@@ -124,11 +176,11 @@ let initializationAttempts = 0 // 初始化尝试次数
 const MAX_INIT_ATTEMPTS = 5 // 最大初始化尝试次数
 
 // 房间尺寸配置(明确边界)
-const roomDimensions: RoomDimensions = {
-  width: 400, // X轴范围:-200 ~ 200
-  length: 400, // Z轴范围:-200 ~ 200
-  height: 280, // Y轴范围:0 ~ 280
-}
+const roomDimensions = ref<RoomDimensions>({
+  length: 400, // 长 Y轴范围:-200 ~ 200
+  width: 400, // 宽 X轴范围:-200 ~ 200
+  height: 280, // 高 Z轴范围:0 ~ 280
+})
 
 /**
  * 安全地释放材质资源
@@ -150,8 +202,12 @@ function disposeMaterial(material: THREE.Material | THREE.Material[] | null) {
  */
 const getRoomBoundingBox = (): THREE.Box3 => {
   return new THREE.Box3(
-    new THREE.Vector3(-roomDimensions.width / 2, 0, -roomDimensions.length / 2), // 最小点
-    new THREE.Vector3(roomDimensions.width / 2, roomDimensions.height, roomDimensions.length / 2) // 最大点
+    new THREE.Vector3(-roomDimensions.value.width / 2, 0, -roomDimensions.value.length / 2), // 最小点
+    new THREE.Vector3(
+      roomDimensions.value.width / 2,
+      roomDimensions.value.height,
+      roomDimensions.value.length / 2
+    ) // 最大点
   )
 }
 
@@ -292,7 +348,7 @@ function setupCameraAndControls() {
   roomBbox.getCenter(roomCenter) // 房间中心点(目标点)
 
   // 调整目标点高度,更符合正视视角
-  roomCenter.y = roomDimensions.height / 3
+  roomCenter.y = roomDimensions.value.height / 3
 
   // 计算刚好能完整显示房间的距离
   const initialDistance = calculateOptimalDistance()
@@ -418,29 +474,29 @@ function createRoom() {
 
   // 后墙
   const backWall = new THREE.Mesh(
-    new THREE.PlaneGeometry(roomDimensions.width, roomDimensions.height),
+    new THREE.PlaneGeometry(roomDimensions.value.width, roomDimensions.value.height),
     glassMat
   )
-  backWall.position.set(0, roomDimensions.height / 2, -roomDimensions.length / 2)
+  backWall.position.set(0, roomDimensions.value.height / 2, -roomDimensions.value.length / 2)
   room.add(backWall)
   glassWalls.push(backWall)
 
   // 左墙
   const leftWall = new THREE.Mesh(
-    new THREE.PlaneGeometry(roomDimensions.length, roomDimensions.height),
+    new THREE.PlaneGeometry(roomDimensions.value.length, roomDimensions.value.height),
     glassMat
   )
-  leftWall.position.set(-roomDimensions.width / 2, roomDimensions.height / 2, 0)
+  leftWall.position.set(-roomDimensions.value.width / 2, roomDimensions.value.height / 2, 0)
   leftWall.rotation.y = Math.PI / 2
   room.add(leftWall)
   glassWalls.push(leftWall)
 
   // 右墙
   const rightWall = new THREE.Mesh(
-    new THREE.PlaneGeometry(roomDimensions.length, roomDimensions.height),
+    new THREE.PlaneGeometry(roomDimensions.value.length, roomDimensions.value.height),
     glassMat
   )
-  rightWall.position.set(roomDimensions.width / 2, roomDimensions.height / 2, 0)
+  rightWall.position.set(roomDimensions.value.width / 2, roomDimensions.value.height / 2, 0)
   rightWall.rotation.y = -Math.PI / 2
   room.add(rightWall)
   glassWalls.push(rightWall)
@@ -474,7 +530,12 @@ function createFloorPoints() {
   if (!scene) return
 
   // 创建平面几何体作为地板
-  const geometry = new THREE.PlaneGeometry(roomDimensions.width, roomDimensions.length, 64, 64)
+  const geometry = new THREE.PlaneGeometry(
+    roomDimensions.value.width,
+    roomDimensions.value.length,
+    64,
+    64
+  )
 
   // 创建点材质的地板
   floorPoints = new THREE.Points(
@@ -561,17 +622,28 @@ function updatePointCloud(rawPoints: [number, number, number, number][]) {
     const colors = new Float32Array(pointCountValue * 3)
 
     // 填充位置和颜色数据
+    // 先找到该帧的最小强度和最大强度
+    const intensities = rawPoints.map((p) => p[3])
+    const minIntensity = Math.min(...intensities)
+    const maxIntensity = Math.max(...intensities)
+
+    // 防止除以 0
+    const range = maxIntensity - minIntensity || 1
+
     rawPoints.forEach((point, index) => {
       const [x, y, z, intensity] = point
       const idx = index * 3
 
-      // 设置位置
-      positions[idx] = x
-      positions[idx + 1] = y
-      positions[idx + 2] = z
+      // 设置位置(原始单位m 需要转为cm * 100)
+      // positions[idx] = x
+      // positions[idx + 1] = y
+      // positions[idx + 2] = z
+      positions[idx] = x * 100
+      positions[idx + 1] = y * 100
+      positions[idx + 2] = z * 100
 
-      // 归一化强度:假设范围是 [1000, 2000]
-      const normalizedIntensity = Math.min(Math.max((intensity - 1000) / 1000, 0), 1)
+      // 动态归一化强度
+      const normalizedIntensity = (intensity - minIntensity) / range
 
       // 获取插值颜色
       const { r, g, b } = getInterpolatedColor(normalizedIntensity)
@@ -605,53 +677,59 @@ function updatePointCloud(rawPoints: [number, number, number, number][]) {
   }
 }
 
-/**
- * 根据强度值获取插值颜色
- * @param value 归一化的强度值(0-1)
- * @returns RGB颜色对象
- */
-function getInterpolatedColor(value: number): RgbColor {
-  // 强度到颜色的渐变映射
-  const intensityGradientSmooth = [
-    { value: 0.0, color: '#0000FF' }, // 蓝
-    { value: 0.25, color: '#00FFFF' }, // 青
-    { value: 0.5, color: '#00FF00' }, // 绿
-    { value: 0.75, color: '#FFFF00' }, // 黄
-    { value: 1.0, color: '#FF0000' }, // 红
-  ]
-
-  // 找到对应的颜色区间并插值
-  for (let i = 0; i < intensityGradientSmooth.length - 1; i++) {
-    const start = intensityGradientSmooth[i]
-    const end = intensityGradientSmooth[i + 1]
-    if (value >= start.value && value <= end.value) {
-      const t = (value - start.value) / (end.value - start.value)
-      const c1 = hexToRgb(start.color)
-      const c2 = hexToRgb(end.color)
-      return {
-        r: c1.r + (c2.r - c1.r) * t,
-        g: c1.g + (c2.g - c1.g) * t,
-        b: c1.b + (c2.b - c1.b) * t,
-      }
-    }
+/** 五段渐变:蓝→青→绿→黄→红(与 Jet 主锚点一致) */
+const intensityGradientSmooth = [
+  { value: 0.0, color: '#0000FF' }, // 蓝
+  { value: 0.25, color: '#00FFFF' }, // 青
+  { value: 0.5, color: '#00FF00' }, // 绿
+  { value: 0.75, color: '#FFFF00' }, // 黄
+  { value: 1.0, color: '#FF0000' }, // 红
+]
+
+// 预计算成 RGB,减少运行时开销
+const gradientStops = intensityGradientSmooth.map((s) => ({
+  value: s.value,
+  rgb: hexToRgb(s.color),
+}))
+
+/** 将十六进制颜色转换为 0–1 的 RGB */
+function hexToRgb(hex: string): RgbColor {
+  const n = parseInt(hex.slice(1), 16)
+  return {
+    r: ((n >> 16) & 255) / 255,
+    g: ((n >> 8) & 255) / 255,
+    b: (n & 255) / 255,
   }
+}
 
-  // 默认返回最后一个颜色
-  return hexToRgb(intensityGradientSmooth[intensityGradientSmooth.length - 1].color)
+/** 线性插值 */
+function lerp(a: number, b: number, t: number) {
+  return a + (b - a) * t
 }
 
 /**
- * 将十六进制颜色转换为RGB颜色
- * @param hex 十六进制颜色字符串
- * @returns RGB颜色对象(通道值0-1)
+ * 已归一化强度(0–1) → 颜色
+ * @param value01 已在外部归一化到 0–1 的强度
+ * @returns RGB (0–1)
  */
-function hexToRgb(hex: string): RgbColor {
-  const bigint = parseInt(hex.slice(1), 16)
-  return {
-    r: ((bigint >> 16) & 255) / 255,
-    g: ((bigint >> 8) & 255) / 255,
-    b: (bigint & 255) / 255,
+function getInterpolatedColor(value01: number): RgbColor {
+  const v = Math.min(Math.max(value01, 0), 1) // clamp 0–1
+
+  // 落在哪个区间
+  for (let i = 0; i < gradientStops.length - 1; i++) {
+    const a = gradientStops[i]
+    const b = gradientStops[i + 1]
+    if (v >= a.value && v <= b.value) {
+      const t = (v - a.value) / (b.value - a.value)
+      return {
+        r: lerp(a.rgb.r, b.rgb.r, t),
+        g: lerp(a.rgb.g, b.rgb.g, t),
+        b: lerp(a.rgb.b, b.rgb.b, t),
+      }
+    }
   }
+  // v==1 的兜底
+  return gradientStops[gradientStops.length - 1].rgb
 }
 
 /**
@@ -802,6 +880,41 @@ function cleanupThreeJs() {
   initialCameraDirection = new THREE.Vector3()
 }
 
+// 在script部分添加applyRoomDimensions函数
+const configPanelVisible = ref(false)
+/**
+ * 应用新的房间尺寸设置
+ * 重新创建房间墙壁和地板,并重置视图
+ */
+function applyRoomDimensions() {
+  if (!scene) return
+
+  // 移除现有的房间墙壁
+  glassWalls.forEach((wall) => {
+    if (wall.parent) {
+      wall.parent.remove(wall)
+    }
+    disposeMaterial(wall.material)
+    wall.geometry.dispose()
+  })
+  glassWalls = []
+
+  // 移除现有的地板点
+  if (floorPoints && floorPoints.parent) {
+    floorPoints.parent.remove(floorPoints)
+    disposeMaterial(floorPoints.material)
+    floorPoints.geometry.dispose()
+    floorPoints = null
+  }
+
+  // 创建新的房间墙壁和地板
+  createRoom()
+  createFloorPoints()
+
+  // 重置视图以适应新的房间尺寸
+  resetView()
+}
+
 // 组件挂载时初始化
 onMounted(() => {
   nextTick().then(() => {
@@ -882,16 +995,54 @@ onUnmounted(() => {
   position: absolute;
   top: 10px;
   right: 10px;
-  color: white;
-  padding: 10px;
-  border-radius: 5px;
+  color: #fff;
+  padding: 12px;
+  width: 200px;
+  font-size: 16px;
   z-index: 10;
 }
 
+.legend-title {
+  margin-bottom: 12px;
+  font-weight: bold;
+}
+
+.legend-content {
+  display: flex;
+  flex-direction: column;
+  align-items: center;
+  margin-bottom: 12px;
+
+  .legend-gradient {
+    width: 100%;
+    height: 20px;
+    background: linear-gradient(
+      to right,
+      #0000ff 0%,
+      #00ffff 25%,
+      #00ff00 50%,
+      #ffff00 75%,
+      #ff0000 100%
+    );
+  }
+
+  .legend-numbers {
+    display: flex;
+    justify-content: space-between;
+    width: 100%;
+    font-size: 12px;
+    margin-top: 4px;
+  }
+}
+
 .legend-item {
   display: flex;
   align-items: center;
   margin: 5px 0;
+
+  &-title {
+    margin-right: 8px;
+  }
 }
 
 .legend-color {
@@ -903,9 +1054,15 @@ onUnmounted(() => {
 
 .control-space {
   color: #fff;
-  font-size: 24px;
-  cursor: pointer;
+  font-size: 20px;
   font-weight: 600;
+
+  :deep(.ant-space-item) {
+    cursor: pointer;
+    &:hover {
+      color: #1677ff;
+    }
+  }
 }
 
 .reset-button:disabled {
@@ -913,6 +1070,32 @@ onUnmounted(() => {
   cursor: not-allowed;
 }
 
+.config-panel {
+  margin-top: 12px;
+  transition: all 0.3s ease;
+}
+.config-title {
+  margin: 0 0 10px 0;
+  font-size: 16px;
+}
+
+.config-item {
+  display: flex;
+  align-items: center;
+  margin-bottom: 8px;
+  &-label {
+    width: 100px;
+    line-height: 24px;
+  }
+  &-input {
+    flex: 1;
+    background-color: transparent;
+    :deep(.ant-input-number-input-wrap) .ant-input-number-input {
+      color: #fff;
+    }
+  }
+}
+
 .info {
   position: absolute;
   bottom: 10px;