|
@@ -11,12 +11,9 @@
|
|
|
class="furniture-item"
|
|
class="furniture-item"
|
|
|
:style="getFurnitureStyle(item)"
|
|
:style="getFurnitureStyle(item)"
|
|
|
>
|
|
>
|
|
|
- <furniture-icon
|
|
|
|
|
- :icon="item.type"
|
|
|
|
|
- :width="item.width"
|
|
|
|
|
- :height="item.length"
|
|
|
|
|
- :draggable="false"
|
|
|
|
|
- />
|
|
|
|
|
|
|
+ <div class="furniture-rotated-container" :style="getRotatedContainerStyle(item)">
|
|
|
|
|
+ <furnitureIcon :icon="item.type" :width="item.width" :height="item.length" />
|
|
|
|
|
+ </div>
|
|
|
</div>
|
|
</div>
|
|
|
</div>
|
|
</div>
|
|
|
|
|
|
|
@@ -114,10 +111,57 @@ const device = {
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
/**
|
|
|
- * 地理坐标转换为像素坐标
|
|
|
|
|
- * @param geoX - 地理坐标系x坐标(厘米)
|
|
|
|
|
- * @param geoY - 地理坐标系y坐标(厘米)
|
|
|
|
|
- * @returns 像素坐标系中的坐标点
|
|
|
|
|
|
|
+ * 计算旋转后的边界框(基于左上角基准)
|
|
|
|
|
+ */
|
|
|
|
|
+const calculateBoundingBox = (width: number, length: number, rotate: number) => {
|
|
|
|
|
+ const rad = (rotate * Math.PI) / 180
|
|
|
|
|
+
|
|
|
|
|
+ if (rotate === 0) {
|
|
|
|
|
+ return {
|
|
|
|
|
+ left: 0,
|
|
|
|
|
+ right: width,
|
|
|
|
|
+ top: 0,
|
|
|
|
|
+ bottom: length,
|
|
|
|
|
+ width: width,
|
|
|
|
|
+ height: length,
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // 计算旋转后的四个角点(相对于左上角)
|
|
|
|
|
+ const corners = [
|
|
|
|
|
+ { x: 0, y: 0 }, // 左上
|
|
|
|
|
+ { x: width, y: 0 }, // 右上
|
|
|
|
|
+ { x: width, y: length }, // 右下
|
|
|
|
|
+ { x: 0, y: length }, // 左下
|
|
|
|
|
+ ]
|
|
|
|
|
+
|
|
|
|
|
+ // 旋转角点(围绕左上角旋转)
|
|
|
|
|
+ const rotatedCorners = corners.map((corner) => ({
|
|
|
|
|
+ x: corner.x * Math.cos(rad) - corner.y * Math.sin(rad),
|
|
|
|
|
+ y: corner.x * Math.sin(rad) + corner.y * Math.cos(rad),
|
|
|
|
|
+ }))
|
|
|
|
|
+
|
|
|
|
|
+ // 计算边界框
|
|
|
|
|
+ const xs = rotatedCorners.map((c) => c.x)
|
|
|
|
|
+ const ys = rotatedCorners.map((c) => c.y)
|
|
|
|
|
+
|
|
|
|
|
+ const minX = Math.min(...xs)
|
|
|
|
|
+ const maxX = Math.max(...xs)
|
|
|
|
|
+ const minY = Math.min(...ys)
|
|
|
|
|
+ const maxY = Math.max(...ys)
|
|
|
|
|
+
|
|
|
|
|
+ return {
|
|
|
|
|
+ left: minX,
|
|
|
|
|
+ right: maxX,
|
|
|
|
|
+ top: minY,
|
|
|
|
|
+ bottom: maxY,
|
|
|
|
|
+ width: maxX - minX,
|
|
|
|
|
+ height: maxY - minY,
|
|
|
|
|
+ }
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+/**
|
|
|
|
|
+ * 地理坐标转换为像素坐标(左上角基准)
|
|
|
*/
|
|
*/
|
|
|
const geoToPixel = (geoX: number, geoY: number): { x: number; y: number } => {
|
|
const geoToPixel = (geoX: number, geoY: number): { x: number; y: number } => {
|
|
|
const radarFurniture: RadarFurniture = {
|
|
const radarFurniture: RadarFurniture = {
|
|
@@ -135,11 +179,44 @@ const geoToPixel = (geoX: number, geoY: number): { x: number; y: number } => {
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
/**
|
|
|
- * 像素坐标转换为地理坐标
|
|
|
|
|
- * @param pixelX - 像素坐标系x坐标
|
|
|
|
|
- * @param pixelY - 像素坐标系y坐标
|
|
|
|
|
- * @returns 地理坐标系中的坐标点
|
|
|
|
|
|
|
+ * 获取家具容器样式 - 基于左上角定位
|
|
|
|
|
+ */
|
|
|
|
|
+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 cssObj = {
|
|
|
|
|
+ position: 'absolute',
|
|
|
|
|
+ left: `${-boundingBox.left}px`,
|
|
|
|
|
+ top: `${-boundingBox.top}px`,
|
|
|
|
|
+ width: `${item.width}px`,
|
|
|
|
|
+ height: `${item.length}px`,
|
|
|
|
|
+ transform: `rotate(${item.rotate || 0}deg)`,
|
|
|
|
|
+ transformOrigin: '0 0',
|
|
|
|
|
+ } as CSSProperties
|
|
|
|
|
+
|
|
|
|
|
+ return cssObj
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
const pixelToGeo = (pixelX: number, pixelY: number): { x: number; y: number } => {
|
|
const pixelToGeo = (pixelX: number, pixelY: number): { x: number; y: number } => {
|
|
|
const canvasRect: CanvasRect = {
|
|
const canvasRect: CanvasRect = {
|
|
|
left: pixelX,
|
|
left: pixelX,
|
|
@@ -170,14 +247,14 @@ const convertAreaByDirection = (
|
|
|
const normalizedDirection = ((direction % 360) + 360) % 360
|
|
const normalizedDirection = ((direction % 360) + 360) % 360
|
|
|
|
|
|
|
|
switch (normalizedDirection) {
|
|
switch (normalizedDirection) {
|
|
|
- case 0: // 设备朝北:x轴反向,y轴保持不变
|
|
|
|
|
- return [-xEnd, -xStart, yStart, yEnd]
|
|
|
|
|
- case 90: // 设备朝东:保持原始坐标系
|
|
|
|
|
|
|
+ case 0: // 设备朝东:保持原始坐标系(x轴朝右)
|
|
|
return [xStart, xEnd, yStart, yEnd]
|
|
return [xStart, xEnd, yStart, yEnd]
|
|
|
- case 180: // 设备朝南:x轴保持不变,y轴反向
|
|
|
|
|
|
|
+ case 90: // 设备朝南:y轴反向
|
|
|
return [xStart, xEnd, -yEnd, -yStart]
|
|
return [xStart, xEnd, -yEnd, -yStart]
|
|
|
- case 270: // 设备朝西:x轴反向,y轴反向
|
|
|
|
|
|
|
+ case 180: // 设备朝西:x轴反向
|
|
|
return [-xEnd, -xStart, -yEnd, -yStart]
|
|
return [-xEnd, -xStart, -yEnd, -yStart]
|
|
|
|
|
+ case 270: // 设备朝北:x轴反向,y轴反向
|
|
|
|
|
+ return [-xEnd, -xStart, yStart, yEnd]
|
|
|
default: // 其他角度使用任意角度转换
|
|
default: // 其他角度使用任意角度转换
|
|
|
return convertAreaForArbitraryAngle(area, direction)
|
|
return convertAreaForArbitraryAngle(area, direction)
|
|
|
}
|
|
}
|
|
@@ -204,12 +281,10 @@ const convertAreaForArbitraryAngle = (
|
|
|
{ x: xEnd, y: yEnd }, // 右上角
|
|
{ x: xEnd, y: yEnd }, // 右上角
|
|
|
]
|
|
]
|
|
|
|
|
|
|
|
- // 计算旋转角度(逆时针旋转)
|
|
|
|
|
const rotationAngle = (-direction * Math.PI) / 180
|
|
const rotationAngle = (-direction * Math.PI) / 180
|
|
|
const cosA = Math.cos(rotationAngle)
|
|
const cosA = Math.cos(rotationAngle)
|
|
|
const sinA = Math.sin(rotationAngle)
|
|
const sinA = Math.sin(rotationAngle)
|
|
|
|
|
|
|
|
- // 应用旋转矩阵变换到所有角点
|
|
|
|
|
const rotatedCorners = corners.map((corner) => {
|
|
const rotatedCorners = corners.map((corner) => {
|
|
|
return {
|
|
return {
|
|
|
x: corner.x * cosA - corner.y * sinA,
|
|
x: corner.x * cosA - corner.y * sinA,
|
|
@@ -217,55 +292,16 @@ const convertAreaForArbitraryAngle = (
|
|
|
}
|
|
}
|
|
|
})
|
|
})
|
|
|
|
|
|
|
|
- // 计算旋转后的边界范围
|
|
|
|
|
const xValues = rotatedCorners.map((c) => c.x)
|
|
const xValues = rotatedCorners.map((c) => c.x)
|
|
|
const yValues = rotatedCorners.map((c) => c.y)
|
|
const yValues = rotatedCorners.map((c) => c.y)
|
|
|
|
|
|
|
|
return [Math.min(...xValues), Math.max(...xValues), Math.min(...yValues), Math.max(...yValues)]
|
|
return [Math.min(...xValues), Math.max(...xValues), Math.min(...yValues), Math.max(...yValues)]
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
-/**
|
|
|
|
|
- * 计算转换后的检测区域坐标(响应式)
|
|
|
|
|
- * 根据当前设备方向实时计算检测区域的实际坐标
|
|
|
|
|
- */
|
|
|
|
|
const convertedCoordinates = computed(() => {
|
|
const convertedCoordinates = computed(() => {
|
|
|
return convertAreaByDirection(props.coordinates, props.direction)
|
|
return convertAreaByDirection(props.coordinates, props.direction)
|
|
|
})
|
|
})
|
|
|
|
|
|
|
|
-/**
|
|
|
|
|
- * 计算家具元素的样式
|
|
|
|
|
- * 将地理坐标系中的家具位置转换为画布中的像素位置和样式
|
|
|
|
|
- * @param item - 家具配置项
|
|
|
|
|
- * @returns 家具的CSS样式对象
|
|
|
|
|
- */
|
|
|
|
|
-const getFurnitureStyle = (item: FurnitureItem) => {
|
|
|
|
|
- const radarFurniture: RadarFurniture = {
|
|
|
|
|
- x: item?.x || 0,
|
|
|
|
|
- y: item?.y || 0,
|
|
|
|
|
- width: item.width,
|
|
|
|
|
- height: item.length,
|
|
|
|
|
- }
|
|
|
|
|
-
|
|
|
|
|
- const canvasRect = convert_furniture_r2c(radarFurniture, radarPosition)
|
|
|
|
|
-
|
|
|
|
|
- return {
|
|
|
|
|
- position: 'absolute',
|
|
|
|
|
- left: `${canvasRect.left - item.width / 2}px`, // 居中定位
|
|
|
|
|
- top: `${canvasRect.top - item.length / 2}px`, // 居中定位
|
|
|
|
|
- width: `${item.width}px`,
|
|
|
|
|
- height: `${item.length}px`,
|
|
|
|
|
- transform: `rotate(${item.rotate}deg)`, // 家具自身旋转
|
|
|
|
|
- transformOrigin: 'center center',
|
|
|
|
|
- pointerEvents: props.mode === 'edit' ? 'auto' : 'none', // 编辑模式下可交互
|
|
|
|
|
- } as CSSProperties
|
|
|
|
|
-}
|
|
|
|
|
-
|
|
|
|
|
-/**
|
|
|
|
|
- * 计算目标点位的样式
|
|
|
|
|
- * 将地理坐标系中的目标位置转换为画布中的像素位置和样式
|
|
|
|
|
- * @param target - 目标点位数据
|
|
|
|
|
- * @returns 目标点位的CSS样式对象
|
|
|
|
|
- */
|
|
|
|
|
const getTargetStyle = (target: TargetPoint) => {
|
|
const getTargetStyle = (target: TargetPoint) => {
|
|
|
const radarFurniture: RadarFurniture = {
|
|
const radarFurniture: RadarFurniture = {
|
|
|
x: target.x,
|
|
x: target.x,
|
|
@@ -276,18 +312,8 @@ const getTargetStyle = (target: TargetPoint) => {
|
|
|
|
|
|
|
|
const canvasRect = convert_furniture_r2c(radarFurniture, radarPosition)
|
|
const canvasRect = convert_furniture_r2c(radarFurniture, radarPosition)
|
|
|
|
|
|
|
|
- // 定义红橙黄绿青蓝紫颜色序列
|
|
|
|
|
- const colorPalette = [
|
|
|
|
|
- 'red', // 0: 红
|
|
|
|
|
- 'orange', // 1: 橙
|
|
|
|
|
- 'yellow', // 2: 黄
|
|
|
|
|
- 'green', // 3: 绿
|
|
|
|
|
- 'cyan', // 4: 青
|
|
|
|
|
- 'blue', // 5: 蓝
|
|
|
|
|
- 'purple', // 6: 紫
|
|
|
|
|
- ]
|
|
|
|
|
|
|
+ const colorPalette = ['red', 'orange', 'yellow', 'green', 'cyan', 'blue', 'purple']
|
|
|
|
|
|
|
|
- // 如果 target.id 超出颜色数组范围,使用最后一个颜色(紫色)
|
|
|
|
|
const colorIndex = target.id < colorPalette.length ? target.id : colorPalette.length - 1
|
|
const colorIndex = target.id < colorPalette.length ? target.id : colorPalette.length - 1
|
|
|
const color = colorPalette[colorIndex]
|
|
const color = colorPalette[colorIndex]
|
|
|
|
|
|
|
@@ -313,10 +339,6 @@ const getTargetStyle = (target: TargetPoint) => {
|
|
|
} as CSSProperties
|
|
} as CSSProperties
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
-/**
|
|
|
|
|
- * 初始化Canvas上下文
|
|
|
|
|
- * 获取Canvas的2D渲染上下文并开始绘制
|
|
|
|
|
- */
|
|
|
|
|
const initCanvas = () => {
|
|
const initCanvas = () => {
|
|
|
if (!canvasRef.value) return
|
|
if (!canvasRef.value) return
|
|
|
|
|
|
|
@@ -367,7 +389,7 @@ const drawRadarDevice = () => {
|
|
|
|
|
|
|
|
// 移动到设备中心并应用旋转
|
|
// 移动到设备中心并应用旋转
|
|
|
ctx.translate(device.x, device.y)
|
|
ctx.translate(device.x, device.y)
|
|
|
- const rotation = props.direction
|
|
|
|
|
|
|
+ const rotation = props.direction + 90
|
|
|
ctx.rotate((rotation * Math.PI) / 180)
|
|
ctx.rotate((rotation * Math.PI) / 180)
|
|
|
|
|
|
|
|
// 绘制居中的雷达图标
|
|
// 绘制居中的雷达图标
|
|
@@ -406,9 +428,9 @@ const drawRadarDeviceFallback = () => {
|
|
|
const indicatorWidth = device.size * 0.3
|
|
const indicatorWidth = device.size * 0.3
|
|
|
|
|
|
|
|
ctx.beginPath()
|
|
ctx.beginPath()
|
|
|
- ctx.moveTo(0, -device.size / 2 - indicatorLength) // 尖角顶点
|
|
|
|
|
- ctx.lineTo(-indicatorWidth / 2, -device.size / 2) // 左下角
|
|
|
|
|
- ctx.lineTo(indicatorWidth / 2, -device.size / 2) // 右下角
|
|
|
|
|
|
|
+ ctx.moveTo(0, -device.size / 2 - indicatorLength)
|
|
|
|
|
+ ctx.lineTo(-indicatorWidth / 2, -device.size / 2)
|
|
|
|
|
+ ctx.lineTo(indicatorWidth / 2, -device.size / 2)
|
|
|
ctx.closePath()
|
|
ctx.closePath()
|
|
|
ctx.fill()
|
|
ctx.fill()
|
|
|
ctx.stroke()
|
|
ctx.stroke()
|
|
@@ -431,22 +453,15 @@ const drawInfoText = () => {
|
|
|
ctx.font = '12px Arial'
|
|
ctx.font = '12px Arial'
|
|
|
ctx.textAlign = 'left'
|
|
ctx.textAlign = 'left'
|
|
|
|
|
|
|
|
- // 文本显示位置(左下角)
|
|
|
|
|
const baseX = 5
|
|
const baseX = 5
|
|
|
const baseY = CANVAS_SIZE - 10
|
|
const baseY = CANVAS_SIZE - 10
|
|
|
|
|
|
|
|
- if (props.direction === 90) {
|
|
|
|
|
- // 默认方向只显示原始坐标
|
|
|
|
|
- ctx.fillText(`检测区域: [${xStart}, ${xEnd}, ${yStart}, ${yEnd}] cm`, baseX, baseY)
|
|
|
|
|
- } else {
|
|
|
|
|
- // 其他方向显示原始和转换后的坐标
|
|
|
|
|
- ctx.fillText(`原始区域: [${xStart}, ${xEnd}, ${yStart}, ${yEnd}] cm`, baseX, baseY - 15)
|
|
|
|
|
- ctx.fillText(
|
|
|
|
|
- `转换区域: [${Math.round(convertedXStart)}, ${Math.round(convertedXEnd)}, ${Math.round(convertedYStart)}, ${Math.round(convertedYEnd)}] cm`,
|
|
|
|
|
- baseX,
|
|
|
|
|
- baseY
|
|
|
|
|
- )
|
|
|
|
|
- }
|
|
|
|
|
|
|
+ ctx.fillText(`原始区域: [${xStart}, ${xEnd}, ${yStart}, ${yEnd}] cm`, baseX, baseY - 15)
|
|
|
|
|
+ ctx.fillText(
|
|
|
|
|
+ `转换区域: [${Math.round(convertedXStart)}, ${Math.round(convertedXEnd)}, ${Math.round(convertedYStart)}, ${Math.round(convertedYEnd)}] cm`,
|
|
|
|
|
+ baseX,
|
|
|
|
|
+ baseY
|
|
|
|
|
+ )
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
/**
|
|
@@ -497,13 +512,11 @@ const drawAreaBorder = () => {
|
|
|
|
|
|
|
|
const [xStart, xEnd, yStart, yEnd] = convertedCoordinates.value
|
|
const [xStart, xEnd, yStart, yEnd] = convertedCoordinates.value
|
|
|
|
|
|
|
|
- // 确保检测区域不超出雷达范围
|
|
|
|
|
const clampedXStart = Math.max(xStart, -RADAR_RANGE / 2)
|
|
const clampedXStart = Math.max(xStart, -RADAR_RANGE / 2)
|
|
|
const clampedXEnd = Math.min(xEnd, RADAR_RANGE / 2)
|
|
const clampedXEnd = Math.min(xEnd, RADAR_RANGE / 2)
|
|
|
const clampedYStart = Math.max(yStart, -RADAR_RANGE / 2)
|
|
const clampedYStart = Math.max(yStart, -RADAR_RANGE / 2)
|
|
|
const clampedYEnd = Math.min(yEnd, RADAR_RANGE / 2)
|
|
const clampedYEnd = Math.min(yEnd, RADAR_RANGE / 2)
|
|
|
|
|
|
|
|
- // 转换到画布坐标系
|
|
|
|
|
const radarRect: RadarRect = {
|
|
const radarRect: RadarRect = {
|
|
|
x_cm_start: clampedXStart,
|
|
x_cm_start: clampedXStart,
|
|
|
x_cm_stop: clampedXEnd,
|
|
x_cm_stop: clampedXEnd,
|
|
@@ -551,10 +564,6 @@ const drawCoordinateAxes = () => {
|
|
|
ctx.setLineDash([]) // 恢复实线样式
|
|
ctx.setLineDash([]) // 恢复实线样式
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
-/**
|
|
|
|
|
- * 监听属性变化
|
|
|
|
|
- * 当坐标、方向或模式变化时重新绘制检测区域
|
|
|
|
|
- */
|
|
|
|
|
watch(
|
|
watch(
|
|
|
() => [props.coordinates, props.direction, props.mode],
|
|
() => [props.coordinates, props.direction, props.mode],
|
|
|
() => {
|
|
() => {
|
|
@@ -582,6 +591,7 @@ defineExpose({
|
|
|
convert_furniture_r2c(furniture, radarPosition),
|
|
convert_furniture_r2c(furniture, radarPosition),
|
|
|
convert_furniture_c2r: (furniture: CanvasRect) => convert_furniture_c2r(furniture, radarPosition),
|
|
convert_furniture_c2r: (furniture: CanvasRect) => convert_furniture_c2r(furniture, radarPosition),
|
|
|
radarPosition,
|
|
radarPosition,
|
|
|
|
|
+ calculateBoundingBox,
|
|
|
})
|
|
})
|
|
|
</script>
|
|
</script>
|
|
|
|
|
|
|
@@ -620,6 +630,22 @@ defineExpose({
|
|
|
|
|
|
|
|
.furniture-item {
|
|
.furniture-item {
|
|
|
position: absolute;
|
|
position: absolute;
|
|
|
|
|
+
|
|
|
|
|
+ .furniture-rotated-container {
|
|
|
|
|
+ position: absolute;
|
|
|
|
|
+
|
|
|
|
|
+ .furniture-wrapper {
|
|
|
|
|
+ width: 100%;
|
|
|
|
|
+ height: 100%;
|
|
|
|
|
+ display: flex;
|
|
|
|
|
+ align-items: center;
|
|
|
|
|
+ justify-content: center;
|
|
|
|
|
+
|
|
|
|
|
+ :deep(*) {
|
|
|
|
|
+ pointer-events: none;
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
}
|
|
}
|
|
|
}
|
|
}
|
|
|
|
|
|