فهرست منبع

fix🐛: 添加热力图、智能巡检修复

gitboyzcf 2 هفته پیش
والد
کامیت
5b2fa1c39d
3فایلهای تغییر یافته به همراه242 افزوده شده و 11 حذف شده
  1. 6 3
      src/layout/Three3D/components/addDevice.vue
  2. 120 1
      src/layout/Three3D/index.vue
  3. 116 7
      src/views/home/middleBottomBox/components/tour.vue

+ 6 - 3
src/layout/Three3D/components/addDevice.vue

@@ -79,10 +79,13 @@
         Y: props.intersection.y, //必传
         Z: props.intersection.z, //必传
         Type: 'FallView', //必传
-        RtspMain: row.sn
+        RtspMain: row.sn,
+        Alias: row.name
       }
-      API_MR_CAMERA_POST(params).then(() => {
-        props.addSprite([{ x: params.X, y: params.Y, z: params.Z, RtspMain: params.RtspMain }])
+      API_MR_CAMERA_POST(params).then((res) => {
+        props.addSprite([
+          { x: params.X, y: params.Y, z: params.Z, RtspMain: params.RtspMain, id: res.id }
+        ])
         emits('closeAddTagModal')
       })
     }

+ 120 - 1
src/layout/Three3D/index.vue

@@ -30,7 +30,7 @@
   import { onMounted, onUnmounted, provide, ref } from 'vue'
   import { useOutsideHomeStore } from '@/stores/modules/home'
   import { useOutsideSystemStore } from '@/stores/modules/system'
-  import { getAutofitScale } from '@/utils'
+  import { getAutofitScale, $mitt } from '@/utils'
   import storage from '@/utils/storage'
   import RightClick from './components/rightClick.vue'
 
@@ -174,6 +174,8 @@
   let axesHelper = null
   let controls = null
   let loader = null
+  let heatmapMesh // 用于控制热力图地形的 Mesh
+
   const init = () => {
     camera = new THREE.PerspectiveCamera(50, window.innerWidth / window.innerHeight, 0.1, 1000)
     // axesHelper = new THREE.AxesHelper(5)
@@ -249,6 +251,7 @@
       scene.environment = texture
       scene.background = 'transparent'
     })
+    createHeatmapTerrain()
     addEvent(renderer.domElement)
   }
 
@@ -291,6 +294,116 @@
     renderer.dispose() // 如果你的renderer对象有dispose方法的话
   }
 
+  function generateHeatmapValue(x, z) {
+    // 增加 scale,使波纹更密集
+    const scale = 0.4
+
+    // 使用两个振幅不同的波叠加,创造更复杂的起伏
+    const value = Math.sin(x * scale) * 0.8 + Math.cos(z * scale * 1.5) * 1.2
+
+    // 我们想要热力值在中心区域更集中、更高。
+    // 使用高斯函数或类似的衰减函数,使中心点 (0, 0) 附近的值更高。
+    const distanceSq = x * x + z * z
+    const centerFactor = Math.exp(-distanceSq * 0.005) // 0.005 控制衰减速度
+
+    // 结合起伏和中心集中度
+    const combinedValue = (value + 2) * 0.5 * centerFactor
+
+    // 确保值在 0 到 1 之间
+    return Math.max(0.0, Math.min(1.0, combinedValue))
+  }
+
+  // 创建热力图地形
+  function createHeatmapTerrain() {
+    // *** 调整尺寸: 进一步缩小到 35x35,并增加分段数使曲面更平滑 ***
+    const width = 35
+    const height = 35
+    const widthSegments = 80 // 增加分段数
+    const heightSegments = 80
+    const maxTerrainHeight = 4 // 增加地形最大高度,使起伏更剧烈
+
+    const geometry = new THREE.PlaneGeometry(width, height, widthSegments, heightSegments)
+    geometry.rotateX(-Math.PI / 2)
+
+    const positionAttribute = geometry.getAttribute('position')
+    const heatmapValues = []
+
+    for (let i = 0; i < positionAttribute.count; i++) {
+      const x = positionAttribute.getX(i)
+      const z = positionAttribute.getZ(i)
+
+      // 1. 获取热力值 (0 到 1 之间)
+      const heatValue = generateHeatmapValue(x, z)
+      heatmapValues.push(heatValue)
+
+      // 2. 根据热力值和坐标生成地形高度
+      // 热力值越高,地形越高,从而将颜色和高度绑定
+      const y = heatValue * maxTerrainHeight + 15 // 整体抬高到建筑上方
+
+      positionAttribute.setY(i, y)
+    }
+
+    geometry.setAttribute('heatValue', new THREE.Float32BufferAttribute(heatmapValues, 1))
+    geometry.computeVertexNormals()
+
+    // 自定义着色器材质(与上次相同,但确保颜色映射和您图片一致)
+    const heatmapMaterial = new THREE.ShaderMaterial({
+      uniforms: {
+        uTime: { value: 0.0 }
+      },
+      vertexShader: `
+            attribute float heatValue;
+            varying float vHeatValue;
+
+            void main() {
+                vHeatValue = heatValue;
+                gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0);
+            }
+        `,
+      fragmentShader: `
+            varying float vHeatValue;
+
+            void main() {
+                vec3 color = vec3(0.0);
+
+                // 颜色映射逻辑: 确保渐变从深蓝 (最低值) 到红色 (最高值)
+                if (vHeatValue < 0.25) {
+                    // 深蓝/黑蓝 到 蓝/青 (对应图中最低部分)
+                    color = mix(vec3(0.0, 0.1, 0.4), vec3(0.0, 0.5, 1.0), vHeatValue * 4.0);
+                } else if (vHeatValue < 0.5) {
+                    // 蓝/青 到 绿色 (对应图中中间部分)
+                    color = mix(vec3(0.0, 0.5, 1.0), vec3(0.0, 1.0, 0.2), (vHeatValue - 0.25) * 4.0);
+                } else if (vHeatValue < 0.75) {
+                    // 绿色 到 黄色 (对应图中高一些的部分)
+                    color = mix(vec3(0.0, 1.0, 0.2), vec3(1.0, 1.0, 0.0), (vHeatValue - 0.5) * 4.0);
+                } else {
+                    // 黄色 到 红色/橙色 (对应图中最高峰部分)
+                    color = mix(vec3(1.0, 1.0, 0.0), vec3(1.0, 0.2, 0.0), (vHeatValue - 0.75) * 4.0);
+                }
+
+                gl_FragColor = vec4(color, 1.0);
+            }
+        `,
+      side: THREE.DoubleSide
+    })
+
+    heatmapMesh = new THREE.Mesh(geometry, heatmapMaterial)
+    heatmapMesh.visible = false
+    heatmapMesh.scale.set(0.2, 0.2, 0.2)
+    scene.add(heatmapMesh)
+  }
+
+  function toggleHeatmapVisibility(visible) {
+    if (heatmapMesh) {
+      heatmapMesh.visible = visible
+      if(visible){
+        translateCamera({ x: 0, y: 10, z: 20 }, { x: 0, y: 0, z: 0 }, 1)
+      }else{
+        translateCamera({ x: 0, y: 2, z: 10 }, { x: 0, y: 0, z: 0 }, 1)
+      }
+    }
+  }
+
   watch(
     () => useHomeStore.temp,
     (newV) => {
@@ -300,6 +413,12 @@
     }
   )
 
+  $mitt.on('onToolsIndex', (v) => {
+    if (v.type === 'rlt') {
+      toggleHeatmapVisibility(v.selected)
+    }
+  })
+
   onMounted(() => {
     init()
     resize()

+ 116 - 7
src/views/home/middleBottomBox/components/tour.vue

@@ -10,7 +10,17 @@
               :style="{ maxWidth: '100%', maxHeight: '100%', aspectRatio: 16 / 9 }"
             />
           </div> -->
-          <pub-video-new :newClass="'t-video' + currentActive" />
+          <pub-video-new v-if="noFull" :newClass="'t-video' + currentActive" />
+          <template v-else>
+            <div
+              v-for="item in fullRoutes"
+              v-show="item.show"
+              :key="item.key"
+              class="absolute top-0 left-0 w-full h-full"
+            >
+              <pub-video-new :newClass="item.className" />
+            </div>
+          </template>
           <!-- 工具面板 -->
           <!-- <operation-panel
           :index="currentActive"
@@ -164,25 +174,124 @@
       }
     })
   }
+  const noFull = computed(() => props.data.type != 'full')
+  const fullRoutes = ref([])
+
+  const videoLoop = async () => {
+    props.data?.route.map((item, i) => {
+      fullRoutes.value.push({
+        ...item,
+        z: 0,
+        show: false,
+        className: 'video' + i + props.currentActive,
+        key: uuid()
+      })
+    })
+    playerObj = {}
+    const play = async (device_id, className, tempArrI) => {
+      try {
+        console.log('websocket请求')
+        const { url } = await useSystem.getStream({ device_id })
+
+        playerObj[tempArrI] = omatVideoPlayer(
+          '.' + className,
+          url,
+          () => {
+            loading.value = false
+          },
+          {
+            enableZoom: false,
+            enableDrag: false
+          }
+        )
+        console.log(playerObj)
+      } catch (error) {
+        console.error(error)
+      }
+    }
+
+    nextTick(async () => {
+      const params1 = fullRoutes.value[tempArrI]
+      params1.show = true
+      loading.value = true
+      console.log(fullRoutes.value)
+
+      await play(params1.address, params1.className, 0)
+
+      if (fullRoutes.value.length < 2) return
+      await play(
+        fullRoutes.value[tempArrI + 1].address,
+        fullRoutes.value[tempArrI + 1].className,
+        1
+      )
+      tempArrT = setInterval(async () => {
+        ++tempArrI
+        console.log(tempArrI)
+
+        if (playerObj[tempArrI - 1]) {
+          fullRoutes.value[tempArrI].show = true
+          fullRoutes.value[tempArrI - 1].show = false
+          closeWorker(tempArrI - 1)
+          playerObj[tempArrI - 1] = null
+          if (tempArrI === fullRoutes.value.length - 1) {
+            tempArrI = -1
+          }
+          await play(
+            fullRoutes.value[tempArrI + 1].address,
+            fullRoutes.value[tempArrI + 1].className,
+            tempArrI + 1
+          )
+        } else {
+          fullRoutes.value[fullRoutes.value.length - 1].show = false
+          fullRoutes.value[tempArrI].show = true
+          closeWorker(fullRoutes.value.length - 1)
+
+          await play(
+            fullRoutes.value[tempArrI + 1].address,
+            fullRoutes.value[tempArrI + 1].className,
+            tempArrI + 1
+          )
+        }
+      }, fullRoutes.value[tempArrI].duration * 1000)
+    })
+  }
 
   const closeVideo = () => {
-    closeWorker()
+    if (noFull.value) {
+      closeWorker()
+    } else {
+      for (let key in playerObj) {
+        closeWorker(key)
+      }
+      playerObj = null
+    }
     clearCom()
   }
   // 关闭线程
   const closeWorker = (index) => {
     if (!playerObj) return
-    clearInterval(tempArrT)
-    playerObj?.destroyed()
-    playerObj = null
+    if (noFull.value) {
+      playerObj?.destroyed()
+    } else {
+      playerObj[index].destroyed()
+      playerObj[index].key = uuid()
+    }
   }
 
   onMounted(() => {
-    videoClick(props.data, props.currentActive)
+    if (noFull.value) {
+      videoClick(props.data, props.currentActive)
+    } else {
+      // 设备协同
+      videoLoop()
+    }
   })
 
   onBeforeUnmount(() => {
-    closeWorker()
+    for (let key in playerObj) {
+      closeWorker(key)
+    }
+    clearInterval(tempArrT)
   })
 </script>
 <style scoped lang="scss"></style>