Jelajahi Sumber

feat(雷达编辑): 增加家具坐标转换和旋转功能

- 在FurnitureItem接口中添加left和top字段用于存储相对位置
- 实现rotateRect和convert_point_r2c方法处理坐标旋转转换
- 更新雷达编辑器组件使用新的坐标转换逻辑
- 调整画布尺寸从400增加到500以适配新功能
- 优化家具显示逻辑,直接使用转换后的坐标
liujia 6 hari lalu
induk
melakukan
8a4d934

+ 4 - 0
src/api/room/types.ts

@@ -63,10 +63,14 @@ export interface FurnitureItem {
   rotate: number // 家具旋转角度
   x: number // 家具距离雷达的x坐标
   y: number // 家具距离雷达的y坐标
+  left: number // 家具距离左侧边相对距离
+  top: number // 家具距离顶点相对距离
 }
 
 export interface LocalFurnitureItem extends FurnitureItem {
   nanoid: string // 家具唯一标识
+  left: number // 家具距离左侧边相对距离
+  top: number // 家具距离顶点相对距离
 }
 
 export interface SubRegionItem {

+ 53 - 41
src/components/DetectionAreaView/index.vue

@@ -1,16 +1,11 @@
 <template>
   <div class="detection-area-view">
-    <canvas ref="canvasRef" width="400" height="400"></canvas>
+    <canvas ref="canvasRef" width="500" height="500"></canvas>
 
     <div class="overlay-layer">
       <template v-if="mode === 'view'">
         <div class="furniture-container" :class="{ 'clip-overflow': mode === 'view' }">
-          <div
-            v-for="item in furnitureItems"
-            :key="item.nanoid"
-            class="furniture-item"
-            :style="getFurnitureStyle(item)"
-          >
+          <div v-for="item in localFurnitureItems" :key="item.nanoid" class="furniture-item">
             <div class="furniture-rotated-container" :style="getRotatedContainerStyle(item)">
               <furnitureIcon :icon="item.type" :width="item.width" :height="item.length" />
             </div>
@@ -50,9 +45,10 @@ import {
   type CanvasRect,
   type RadarPosition,
   type RadarFurniture,
+  rotateRect,
 } from '@/utils/coordTransform'
 import radarUrl from '@/assets/furnitures/radar.png'
-import type { FurnitureItem, LocalFurnitureItem } from '@/api/room/types'
+import type { LocalFurnitureItem } from '@/api/room/types'
 
 defineOptions({ name: 'DetectionAreaView' })
 
@@ -81,8 +77,8 @@ const props = withDefaults(defineProps<Props>(), {
 })
 
 // 画布配置常量
-const CANVAS_SIZE = 400 // 画布尺寸(像素)
-const RADAR_RANGE = 400 // 雷达检测范围(厘米)
+const CANVAS_SIZE = 500 // 画布尺寸(像素)
+const RADAR_RANGE = 500 // 雷达检测范围(厘米)
 
 // Canvas 相关引用
 const canvasRef = ref<HTMLCanvasElement | null>(null)
@@ -111,6 +107,44 @@ const device = {
   size: 20, // 设备图标尺寸
 }
 
+const localFurnitureItems = computed(() => {
+  return props.furnitureItems.map((item) => {
+    const itemConvert = convert_furniture_r2c(
+      {
+        x: item.x,
+        y: item.y,
+        width: item.width,
+        height: item.length,
+      },
+      radarPosition
+    )
+    const rotatedRect = rotateRect(
+      {
+        left: itemConvert.left,
+        top: itemConvert.top,
+        width: item.width,
+        height: item.length,
+      },
+      {
+        x: CANVAS_SIZE / 2,
+        y: CANVAS_SIZE / 2,
+      },
+      props.direction
+    )
+    return {
+      ...item,
+      left: rotatedRect.left,
+      top: rotatedRect.top,
+      width: rotatedRect.width,
+      length: rotatedRect.height,
+    }
+  })
+})
+
+watch(localFurnitureItems, (newItems) => {
+  console.log('🔥🔥🔥🔥🔥🔥🔥🔥🔥localFurnitureItems', newItems)
+})
+
 /**
  * 计算旋转后的边界框(基于左上角基准)
  */
@@ -180,35 +214,13 @@ const geoToPixel = (geoX: number, geoY: number): { x: number; y: number } => {
 }
 
 /**
- * 获取家具容器样式 - 基于左上角定位
- */
-const getFurnitureStyle = (item: FurnitureItem) => {
-  // 获取家具左上角的像素坐标
-  const pixelPos = geoToPixel(item?.x || 0, item?.y || 0)
-  const boundingBox = calculateBoundingBox(item.width, item.length, item.rotate || 0)
-
-  const cssObj = {
-    position: 'absolute',
-    left: `${pixelPos.x}px`,
-    top: `${pixelPos.y}px`,
-    width: `${boundingBox.width}px`,
-    height: `${boundingBox.height}px`,
-    pointerEvents: props.mode === 'edit' ? 'auto' : 'none',
-  } as CSSProperties
-
-  return cssObj
-}
-
-/**
  * 获取旋转容器样式 - 调整位置使内容在边界框中正确显示
  */
-const getRotatedContainerStyle = (item: FurnitureItem) => {
-  const boundingBox = calculateBoundingBox(item.width, item.length, item.rotate || 0)
-
+const getRotatedContainerStyle = (item: LocalFurnitureItem) => {
   const cssObj = {
     position: 'absolute',
-    left: `${-boundingBox.left}px`,
-    top: `${-boundingBox.top}px`,
+    left: `${item.left}px`,
+    top: `${item.top}px`,
     width: `${item.width}px`,
     height: `${item.length}px`,
     transform: `rotate(${item.rotate || 0}deg)`,
@@ -251,11 +263,11 @@ const convertAreaByDirection = (
     case 0: // 设备朝东:保持原始坐标系(x轴朝右)
       return [xStart, xEnd, yStart, yEnd]
     case 90: // 设备朝南:y轴反向
-      return [xStart, xEnd, -yEnd, -yStart]
+      return [yEnd, yStart, -xEnd, -xStart]
     case 180: // 设备朝西:x轴反向
       return [-xEnd, -xStart, -yEnd, -yStart]
     case 270: // 设备朝北:x轴反向,y轴反向
-      return [-xEnd, -xStart, yStart, yEnd]
+      return [-yEnd, -yStart, xStart, xEnd]
     default: // 其他角度使用任意角度转换
       return convertAreaForArbitraryAngle(area, direction)
   }
@@ -600,8 +612,8 @@ defineExpose({
 .detection-area-view {
   position: relative;
   display: inline-block;
-  width: 400px;
-  height: 400px;
+  width: 500px;
+  height: 500px;
 
   canvas {
     border: 1px solid #ddd;
@@ -613,8 +625,8 @@ defineExpose({
     position: absolute;
     top: 0;
     left: 0;
-    width: 400px;
-    height: 400px;
+    width: 100%;
+    height: 100%;
     pointer-events: auto;
 
     .furniture-container {

+ 2 - 3
src/components/EditableFurniture/index.vue

@@ -24,7 +24,7 @@ interface Props {
 }
 
 const props = withDefaults(defineProps<Props>(), {
-  canvasSize: 400,
+  canvasSize: 500,
   initialAtOrigin: false,
   disabled: false,
 })
@@ -111,7 +111,7 @@ const localItem = reactive<FurnitureItem>({
 })
 
 // 像素位置和边界框
-const pixelPosition = reactive({ left: 0, top: 0 })
+const pixelPosition = reactive({ left: props.item.left ?? 0, top: props.item.top ?? 0 })
 
 // 计算属性
 const boundingBox = computed(() => calculateBoundingBox())
@@ -229,7 +229,6 @@ onMounted(() => {
 
 // 拖拽家具
 const startDrag = (e: MouseEvent) => {
-  console.log('Start drag furniture (top-left):', localItem.nanoid)
   e.preventDefault()
   e.stopPropagation()
 

+ 15 - 18
src/components/RadarEditor/index.vue

@@ -8,7 +8,7 @@
           :item="item"
           :angle="angle"
           :coordinates="coordinates"
-          :canvas-size="400"
+          :canvas-size="500"
           :disabled="disabled"
           @update="handleFurnitureUpdate"
         />
@@ -16,13 +16,13 @@
 
       <template #subregion>
         <EditableSubregion
-          v-if="modeRadio === 2"
+          v-if="showSubregion"
           ref="editableSubregionRef"
           :angle="angle"
-          :canvas-size="400"
+          :canvas-size="500"
           :subRegions="roomStore.localSubRegions"
           :editable="!disabled"
-          :has-bed="localFurniture.some((item) => item.type === 'bed')"
+          :has-bed="roomStore.localFurnitureItems.some((item) => item.type === 'bed')"
           @create="handleSubregionCreate"
           @update="handleSubregionUpdate"
         />
@@ -87,7 +87,7 @@
                   >新建区域</a-button
                 >
               </div>
-              <div v-else>
+              <template v-else>
                 <span>已创建 {{ roomStore.localSubRegions.length }} 个区域</span>
                 <a-button
                   v-if="roomStore.localSubRegions.length < 6"
@@ -96,7 +96,14 @@
                   @click="createSubregion"
                   >继续创建</a-button
                 >
-              </div>
+                <a-switch
+                  v-model:checked="showSubregion"
+                  size="small"
+                  style="margin-left: 20px"
+                  checked-children="显示"
+                  un-checked-children="隐藏"
+                />
+              </template>
             </div>
           </div>
         </div>
@@ -385,12 +392,7 @@ import type { FurnitureIconType } from '@/types/furniture'
 // import { message } from 'ant-design-vue'
 import { nanoid } from 'nanoid'
 import { furnitureIconNameMap, furnitureIconSizeMap } from '@/const/furniture'
-import type {
-  FurnitureItem,
-  LocalFurnitureItem,
-  LocalSubRegionItem,
-  // SubRegions,
-} from '@/api/room/types'
+import type { LocalFurnitureItem, LocalSubRegionItem } from '@/api/room/types'
 import {
   DeleteOutlined,
   // ArrowLeftOutlined,
@@ -408,14 +410,8 @@ interface Props {
   disabled?: boolean
 }
 const props = defineProps<Props>()
-// const emit = defineEmits<{
-//   (e: 'update:furnitureItems', items: FurnitureItem[]): void
-//   (e: 'update:subRegions', regions: SubRegions[]): void
-// }>()
 
 const roomStore = useRoomStore()
-const localFurniture = ref<FurnitureItem[]>(roomStore.localFurnitureItems ?? [])
-// const localSubRegions = ref<SubRegions[]>(roomStore.localSubRegions ?? [])
 
 console.log('props', props)
 
@@ -549,6 +545,7 @@ const handleSubregionUpdate = (item: LocalSubRegionItem[]) => {
 }
 
 const showPanel = ref(true)
+const showSubregion = ref(false)
 
 onUnmounted(() => {
   // 组件销毁时清除缓存

+ 98 - 0
src/utils/coordTransform.ts

@@ -121,3 +121,101 @@ export function convert_furniture_c2r(
     height,
   }
 }
+
+/* =====================  旋转方法 =================================== */
+interface Rect {
+  left: number
+  top: number
+  width: number
+  height: number
+}
+
+interface Point {
+  x: number
+  y: number
+}
+
+/**
+ * 顺时针旋转矩形
+ * @param src_rect - 输入矩形 { left, top, width, height }
+ * @param pRadar - 雷达中心坐标 { x, y }
+ * @param angle - 顺时针旋转角度 0, 90, 180, 270
+ * @returns 旋转后的矩形 { left, top, width, height }
+ */
+export function rotateRect(src_rect: Rect, pRadar: Point, angle: number): Rect {
+  if (![0, 90, 180, 270].includes(angle)) angle = 0
+
+  const { left, top, width, height } = src_rect
+  const cx = left + width / 2
+  const cy = top + height / 2
+
+  const dx = cx - pRadar.x
+  const dy = cy - pRadar.y
+
+  let new_dx: number = 0
+  let new_dy: number = 0
+
+  switch (angle) {
+    case 0:
+      new_dx = dx
+      new_dy = dy
+      break
+    case 90:
+      new_dx = -dy
+      new_dy = dx
+      break
+    case 180:
+      new_dx = -dx
+      new_dy = -dy
+      break
+    case 270:
+      new_dx = dy
+      new_dy = -dx
+      break
+  }
+
+  const new_cx = pRadar.x + new_dx
+  const new_cy = pRadar.y + new_dy
+
+  let new_width = width
+  let new_height = height
+  if (angle === 90 || angle === 270) {
+    new_width = height
+    new_height = width
+  }
+
+  return {
+    left: new_cx - new_width / 2,
+    top: new_cy - new_height / 2,
+    width: new_width,
+    height: new_height,
+  }
+}
+
+interface Point {
+  x: number
+  y: number
+}
+
+/**
+ * 将雷达坐标系中的点转换到画布坐标系(旋转 + 平移)
+ * @param src_point - 雷达坐标系中的点
+ * @param pRadar - 雷达在画布坐标系中的坐标
+ * @param angle - 雷达顺时针旋转角度(度)
+ * @returns 画布坐标系中的点
+ */
+export function convert_point_r2c(src_point: Point, pRadar: Point, angle: number): Point {
+  const rad = (angle * Math.PI) / 180 // 角度转弧度(顺时针为正)
+  const cosA = Math.cos(rad)
+  const sinA = Math.sin(rad)
+
+  // 顺时针旋转
+  const xRot = src_point.x * cosA + src_point.y * sinA
+  const yRot = -src_point.x * sinA + src_point.y * cosA
+
+  // 平移到画布坐标系
+  return {
+    x: pRadar.x + xRot,
+    y: pRadar.y - yRot,
+  }
+}

+ 37 - 0
src/views/device/detail/components/deviceAreaConfig/index.vue

@@ -46,6 +46,7 @@ import * as roomApi from '@/api/room'
 import { message } from 'ant-design-vue'
 import RadarEditor from '@/components/RadarEditor/index.vue'
 import { useRoomStore } from '@/stores/room'
+import { convert_furniture_r2c, rotateRect } from '@/utils/coordTransform'
 
 defineOptions({
   name: 'deviceAreaConfig',
@@ -111,6 +112,42 @@ const fetchRoomLayout = async () => {
     roomStore.cacheFurniture(furnitures ?? [])
     roomStore.cacheSubRegion(subRegions ?? [])
     spinning.value = false
+
+    roomStore.localFurnitureItems = roomStore.localFurnitureItems.map((item) => {
+      const itemConvert = convert_furniture_r2c(
+        {
+          x: item.x,
+          y: item.y,
+          width: item.width,
+          height: item.length,
+        },
+        {
+          x_radar: 250,
+          y_radar: 250,
+        }
+      )
+      const rotatedRect = rotateRect(
+        {
+          left: itemConvert.left,
+          top: itemConvert.top,
+          width: item.width,
+          height: item.length,
+        },
+        {
+          x: 250,
+          y: 250,
+        },
+        props.angle
+      )
+
+      return {
+        ...item,
+        left: rotatedRect.left,
+        top: rotatedRect.top,
+        width: rotatedRect.width,
+        length: rotatedRect.height,
+      }
+    })
   } catch (error) {
     console.error('❌获取房间布局信息失败', error)
     spinning.value = false

+ 1 - 1
src/views/device/detail/components/deviceConfig/index.vue

@@ -89,7 +89,7 @@ const props = withDefaults(defineProps<Props>(), {
 
 const modeRadio = ref<'base' | 'area'>(props.mode)
 const drawerWidth = computed(() => {
-  return modeRadio.value === 'base' ? 600 : 800
+  return modeRadio.value === 'base' ? 600 : 900
 })
 const drawerTitle = computed(() =>
   props.title ? props.title : modeRadio.value === 'base' ? '设备配置' : '区域配置'

+ 7 - 4
src/views/device/detail/index.vue

@@ -287,6 +287,7 @@ const simpleImage = Empty.PRESENTED_IMAGE_SIMPLE
 import { useDict } from '@/hooks/useDict'
 import DeviceUpgrade from './components/DeviceUpgrade/index.vue'
 import { useRoomStore } from '@/stores/room'
+import { convert_point_r2c } from '@/utils/coordTransform'
 
 defineOptions({
   name: 'DeviceDetail',
@@ -508,6 +509,8 @@ const subscribePoint = () => {
           if (item.length < 4) return
           const [x, y, z, id] = item
           currentIds.add(id)
+          const pRadar = { x: 250, y: 250 }
+          const point = convert_point_r2c({ x, y }, pRadar, Number(detailState.value.northAngle))
           if (!(id in targets)) {
             // targets[id] = { x, y, z, id, displayX: x, displayY: y, lastX: x, lastY: y }
             targets[id] = {
@@ -515,8 +518,8 @@ const subscribePoint = () => {
               y,
               z,
               id,
-              displayX: x - Number(detailState.value.xxStart),
-              displayY: y - Number(detailState.value.yyEnd),
+              displayX: point.x - Number(detailState.value.xxStart),
+              displayY: point.y - Number(detailState.value.yyEnd),
               lastX: x,
               lastY: y,
             }
@@ -532,8 +535,8 @@ const subscribePoint = () => {
             targets[id].lastY = y
             // targets[id].displayX = x
             // targets[id].displayY = y
-            targets[id].displayX = x - Number(detailState.value.xxStart)
-            targets[id].displayY = y - Number(detailState.value.yyEnd)
+            targets[id].displayX = point.x - Number(detailState.value.xxStart)
+            targets[id].displayY = point.y - Number(detailState.value.yyEnd)
             // console.log(`🔄 更新目标点: id=${id}, x=${x}, y=${y}`, targets[id])
           } else {
             // 距离太小,忽略本次更新