feat(auto-door): 更新自动门设备状态模拟逻辑,优化门区域配色与状态显示
This commit is contained in:
parent
151534ae38
commit
7d0abea45e
@ -4,5 +4,5 @@ ENV_WEBSOCKET_BASE=/ws
|
||||
ENV_STORAGE_WEBSOCKET_BASE=/vwedWs
|
||||
|
||||
# 开发环境token配置 - 可以手动设置或从另一个项目获取后填入
|
||||
ENV_DEV_TOKEN=eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJleHAiOjE3NjAxMDE2MTgsInVzZXJuYW1lIjoiYWRtaW4ifQ.e9kehve_MAqVPDRHRpMsJp2rEgyPW5pz_s0XDYUoxyk
|
||||
ENV_DEV_TOKEN=eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJleHAiOjE3NjExNjM1NjQsInVzZXJuYW1lIjoiYWRtaW4ifQ.zJ0CJJzwxX2ZptxZjLfOL6upjrqYzdn3yQlOtQzO85A
|
||||
ENV_DEV_TENANT_ID=1000
|
||||
|
||||
BIN
src/assets/icons/png/guanmen.png
Normal file
BIN
src/assets/icons/png/guanmen.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 574 B |
BIN
src/assets/icons/png/weixinkaimen.png
Normal file
BIN
src/assets/icons/png/weixinkaimen.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 418 B |
@ -80,6 +80,8 @@ const client = shallowRef<WebSocket>();
|
||||
// 模拟门设备WS推送(仅开发调试使用)
|
||||
let doorMockTimer: number | undefined;
|
||||
let doorMockStatus: 0 | 1 = 0;
|
||||
// 模拟门设备断连/连通的节奏控制
|
||||
let doorMockTick = 0;
|
||||
// 左侧边栏元素引用(用于 Ctrl/Cmd+F 聚焦搜索框)
|
||||
const leftSiderEl = shallowRef<HTMLElement>();
|
||||
const isPlaybackControllerVisible = ref<boolean>(true);
|
||||
@ -406,7 +408,16 @@ const monitorScene = async () => {
|
||||
//
|
||||
}
|
||||
doorMockTimer = window.setInterval(() => {
|
||||
doorMockStatus = doorMockStatus === 0 ? 1 : 0;
|
||||
doorMockTick++;
|
||||
// 每 7 个周期(约14秒)有 2 个周期(约4秒)模拟断连
|
||||
const phase = doorMockTick % 7;
|
||||
const mockIsConnected = !(phase === 0 || phase === 1);
|
||||
// 仅在连接状态下切换开/关门,断连时保持关门(0)
|
||||
if (mockIsConnected) {
|
||||
doorMockStatus = doorMockStatus === 0 ? 1 : 0;
|
||||
} else {
|
||||
doorMockStatus = 0;
|
||||
}
|
||||
const mock: AutoDoorWebSocketData = {
|
||||
gid: 'mock-gid',
|
||||
id: mockDeviceId,
|
||||
@ -415,7 +426,7 @@ const monitorScene = async () => {
|
||||
type: 99,
|
||||
ip: null,
|
||||
battery: 100,
|
||||
isConnected: true,
|
||||
isConnected: mockIsConnected,
|
||||
state: 0,
|
||||
canOrder: true,
|
||||
canStop: null,
|
||||
|
||||
@ -313,23 +313,25 @@ const DEFAULT_COLORS: EditorColorConfig = {
|
||||
},
|
||||
area: {
|
||||
strokeActive: '#EBB214',
|
||||
stroke: generateAreaStrokeColors(),
|
||||
fill: generateAreaFillColors(),
|
||||
// 为门区域(15)定制更友好的配色:蓝色系,提升可读性
|
||||
stroke: { ...generateAreaStrokeColors(), 15: '#1890FF99' },
|
||||
fill: { ...generateAreaFillColors(), 15: '#1890FF33' },
|
||||
// 边框配置
|
||||
border: {
|
||||
width: 1, // 默认边框宽度
|
||||
opacity: 0.15, // 默认边框透明度 15%
|
||||
colors: generateAreaBorderColors(),
|
||||
colors: { ...generateAreaBorderColors(), 15: '#1890FF' },
|
||||
},
|
||||
types: {
|
||||
...generateAreaTypeColors(),
|
||||
// 门区域(15)专属:统一蓝色系,边框由上方 colors[15] 提供
|
||||
15: {
|
||||
stroke: '#52C41A',
|
||||
strokeActive: '#52C41A',
|
||||
fill: '#52C41A33',
|
||||
borderColor: '#52C41A',
|
||||
stroke: '#1890FF99',
|
||||
strokeActive: '#1890FF99',
|
||||
fill: '#1890FF33',
|
||||
borderColor: '#1890FF',
|
||||
borderWidth: 1,
|
||||
borderOpacity: 0.3,
|
||||
borderOpacity: 0.22,
|
||||
},
|
||||
},
|
||||
},
|
||||
@ -420,6 +422,7 @@ const DARK_THEME_COLORS: EditorColorConfig = {
|
||||
12: '#0DBB8A99',
|
||||
13: '#e61e4aad',
|
||||
14: '#FFD70099',
|
||||
15: '#1890FF99',
|
||||
},
|
||||
fill: {
|
||||
1: '#9ACDFF33',
|
||||
@ -427,6 +430,7 @@ const DARK_THEME_COLORS: EditorColorConfig = {
|
||||
12: '#0DBB8A33',
|
||||
13: '#e61e4a33',
|
||||
14: '#FFD70033',
|
||||
15: '#1890FF26',
|
||||
},
|
||||
// 边框配置
|
||||
border: {
|
||||
@ -438,6 +442,7 @@ const DARK_THEME_COLORS: EditorColorConfig = {
|
||||
12: '#52C41A',
|
||||
13: '#FA8C16',
|
||||
14: '#722ED1',
|
||||
15: '#1890FF',
|
||||
},
|
||||
},
|
||||
types: {
|
||||
@ -481,6 +486,14 @@ const DARK_THEME_COLORS: EditorColorConfig = {
|
||||
borderWidth: 1,
|
||||
borderOpacity: 0.15,
|
||||
},
|
||||
15: {
|
||||
stroke: '#1890FF99',
|
||||
strokeActive: '#FCC947',
|
||||
fill: '#1890FF26',
|
||||
borderColor: '#1890FF',
|
||||
borderWidth: 1,
|
||||
borderOpacity: 0.2,
|
||||
},
|
||||
},
|
||||
},
|
||||
robot: {
|
||||
|
||||
@ -1,446 +1,451 @@
|
||||
import {
|
||||
MapAreaType,
|
||||
type MapPen,
|
||||
MapPointType,
|
||||
MapRoutePassType,
|
||||
MapRouteType,
|
||||
type Point,
|
||||
} from '@api/map';
|
||||
import { DOOR_AREA_TYPE } from '@api/map/door-area';
|
||||
import sTheme from '@core/theme.service';
|
||||
import { type Meta2dStore } from '@meta2d/core';
|
||||
import { get } from 'lodash-es';
|
||||
|
||||
import colorConfig from '../color/color-config.service';
|
||||
|
||||
export function drawPoint(ctx: CanvasRenderingContext2D, pen: MapPen): void {
|
||||
const theme = sTheme.editor;
|
||||
const { active, iconSize: r = 0, fontSize = 14, lineHeight = 1.5, fontFamily } = pen.calculative ?? {};
|
||||
const { x = 0, y = 0, width: w = 0, height: h = 0 } = pen.calculative?.worldRect ?? {};
|
||||
const { type, isConnected, deviceStatus, active: pointActive } = pen.point ?? {};
|
||||
const { label = '', statusStyle } = pen ?? {};
|
||||
|
||||
ctx.save();
|
||||
|
||||
// 自动门点:根据连接与开关状态绘制矩形光圈(无边框)
|
||||
if (type === MapPointType.自动门点 && pointActive) {
|
||||
// 让光圈随点位尺寸等比缩放,避免缩放画布时视觉上变大
|
||||
const base = Math.min(w, h);
|
||||
const padding = Math.max(2, Math.min(10, base * 0.2));
|
||||
const rx = x - padding;
|
||||
const ry = y - padding;
|
||||
const rw = w + padding * 2;
|
||||
const rh = h + padding * 2;
|
||||
|
||||
// 使用与点位相同的圆角半径,使观感统一
|
||||
ctx.beginPath();
|
||||
ctx.roundRect(rx, ry, rw, rh, r);
|
||||
|
||||
if (isConnected === false) {
|
||||
// 未连接:深红色实心,不描边
|
||||
ctx.fillStyle = colorConfig.getColor('autoDoor.strokeClosed') || '#fe5a5ae0';
|
||||
} else {
|
||||
// 已连接:根据门状态显示颜色(0=关门-浅红,1=开门-蓝色)
|
||||
if (deviceStatus === 0) {
|
||||
ctx.fillStyle = colorConfig.getColor('autoDoor.fillClosed') || '#cddc39';
|
||||
} else {
|
||||
ctx.fillStyle = colorConfig.getColor('autoDoor.fillOpen') || '#1890FF';
|
||||
}
|
||||
}
|
||||
ctx.fill();
|
||||
// 重置路径,避免后续对点位边框的 stroke 影响到光圈路径
|
||||
ctx.beginPath();
|
||||
}
|
||||
|
||||
switch (type) {
|
||||
case MapPointType.普通点:
|
||||
case MapPointType.等待点:
|
||||
case MapPointType.避让点:
|
||||
case MapPointType.临时避让点:
|
||||
case MapPointType.库区点:
|
||||
case MapPointType.不可避让点: {
|
||||
ctx.beginPath();
|
||||
ctx.moveTo(x + w / 2 - r, y + r);
|
||||
ctx.arcTo(x + w / 2, y, x + w - r, y + h / 2 - r, r);
|
||||
ctx.arcTo(x + w, y + h / 2, x + w / 2 + r, y + h - r, r);
|
||||
ctx.arcTo(x + w / 2, y + h, x + r, y + h / 2 + r, r);
|
||||
ctx.arcTo(x, y + h / 2, x + r, y + h / 2 - r, r);
|
||||
ctx.closePath();
|
||||
// 优先使用小点位专用颜色,如果没有则使用类型专用颜色
|
||||
const smallGeneralColor = colorConfig.getColor(`point.small.fill.${type}`);
|
||||
const smallTypeColor = colorConfig.getColor(`point.types.${type}.fill`);
|
||||
const smallThemeColor = get(theme, `point-s.fill-${type}`);
|
||||
const finalColor = smallGeneralColor || smallTypeColor || smallThemeColor || '';
|
||||
|
||||
ctx.fillStyle = finalColor;
|
||||
ctx.fill();
|
||||
|
||||
const smallTypeStrokeColor = colorConfig.getColor(`point.types.${type}.stroke`);
|
||||
const smallGeneralStrokeColor = colorConfig.getColor(active ? 'point.small.strokeActive' : 'point.small.stroke');
|
||||
const smallThemeStrokeColor = get(theme, active ? 'point-s.strokeActive' : 'point-s.stroke');
|
||||
ctx.strokeStyle = smallTypeStrokeColor || smallGeneralStrokeColor || smallThemeStrokeColor || '';
|
||||
if (type === MapPointType.临时避让点) {
|
||||
ctx.lineCap = 'round';
|
||||
ctx.beginPath();
|
||||
ctx.moveTo(x + 0.66 * r, y + h / 2 - 0.66 * r);
|
||||
ctx.lineTo(x + r, y + h / 2 - r);
|
||||
ctx.moveTo(x + w / 2 - 0.66 * r, y + 0.66 * r);
|
||||
ctx.lineTo(x + w / 2 - r, y + r);
|
||||
ctx.moveTo(x + w / 2 + 0.66 * r, y + 0.66 * r);
|
||||
ctx.lineTo(x + w / 2 + r, y + r);
|
||||
ctx.moveTo(x + w - 0.66 * r, y + h / 2 - 0.66 * r);
|
||||
ctx.lineTo(x + w - r, y + h / 2 - r);
|
||||
ctx.moveTo(x + w - 0.66 * r, y + h / 2 + 0.66 * r);
|
||||
ctx.lineTo(x + w - r, y + h / 2 + r);
|
||||
ctx.moveTo(x + w / 2 + 0.66 * r, y + h - 0.66 * r);
|
||||
ctx.lineTo(x + w / 2 + r, y + h - r);
|
||||
ctx.moveTo(x + w / 2 - 0.66 * r, y + h - 0.66 * r);
|
||||
ctx.lineTo(x + w / 2 - r, y + h - r);
|
||||
ctx.moveTo(x + 0.66 * r, y + h / 2 + 0.66 * r);
|
||||
ctx.lineTo(x + r, y + h / 2 + r);
|
||||
}
|
||||
ctx.stroke();
|
||||
break;
|
||||
}
|
||||
case MapPointType.电梯点:
|
||||
case MapPointType.自动门点:
|
||||
case MapPointType.充电点:
|
||||
case MapPointType.停靠点:
|
||||
case MapPointType.动作点:
|
||||
case MapPointType.禁行点: {
|
||||
ctx.roundRect(x, y, w, h, r);
|
||||
|
||||
// 优先使用类型专用颜色,如果没有则使用通用颜色
|
||||
const largeTypeStrokeColor = colorConfig.getColor(`point.types.${type}.stroke`);
|
||||
const largeGeneralStrokeColor = colorConfig.getColor(active ? 'point.large.strokeActive' : 'point.large.stroke');
|
||||
const largeThemeStrokeColor = get(theme, active ? 'point-l.strokeActive' : 'point-l.stroke');
|
||||
ctx.strokeStyle = statusStyle ?? (largeTypeStrokeColor || largeGeneralStrokeColor || largeThemeStrokeColor || '');
|
||||
ctx.stroke();
|
||||
break;
|
||||
}
|
||||
default:
|
||||
break;
|
||||
}
|
||||
ctx.fillStyle = colorConfig.getColor('common.color') || (get(theme, 'color') ?? '');
|
||||
ctx.font = `${fontSize}px/${lineHeight} ${fontFamily}`;
|
||||
ctx.textAlign = 'center';
|
||||
ctx.textBaseline = 'top';
|
||||
ctx.fillText(label, x + w / 2, y - fontSize * lineHeight);
|
||||
|
||||
ctx.restore();
|
||||
}
|
||||
|
||||
export function anchorPoint(pen: MapPen): void {
|
||||
pen.anchors = [
|
||||
{ penId: pen.id, id: '0', x: 0.5, y: 0.5 },
|
||||
];
|
||||
}
|
||||
|
||||
export function drawLine(ctx: CanvasRenderingContext2D, pen: MapPen): void {
|
||||
const theme = sTheme.editor;
|
||||
const { active, lineWidth: s = 1 } = pen.calculative ?? {};
|
||||
const [p1, p2] = pen.calculative?.worldAnchors ?? [];
|
||||
const { x: x1 = 0, y: y1 = 0 } = p1 ?? {};
|
||||
const { x: x2 = 0, y: y2 = 0 } = p2 ?? {};
|
||||
const { type, direction = 1, pass = 0, c1, c2 } = pen.route ?? {};
|
||||
const { x: dx1 = 0, y: dy1 = 0 } = c1 ?? {};
|
||||
const { x: dx2 = 0, y: dy2 = 0 } = c2 ?? {};
|
||||
const [c1x, c1y] = [x1 + dx1 * s, y1 + dy1 * s];
|
||||
const [c2x, c2y] = [x2 + dx2 * s, y2 + dy2 * s];
|
||||
|
||||
ctx.save();
|
||||
ctx.beginPath();
|
||||
// 根据路线通行类型获取颜色
|
||||
let routeColor = '';
|
||||
if (active) {
|
||||
routeColor = colorConfig.getColor('route.strokeActive') || get(theme, 'route.strokeActive') || '';
|
||||
} else {
|
||||
// 根据通行类型选择颜色
|
||||
switch (pass) {
|
||||
case MapRoutePassType.无:
|
||||
routeColor = colorConfig.getColor('route.strokeNone') || get(theme, 'route.stroke-0') || '';
|
||||
break;
|
||||
case MapRoutePassType.仅空载可通行:
|
||||
routeColor = colorConfig.getColor('route.strokeEmpty') || get(theme, 'route.stroke-empty') || '';
|
||||
break;
|
||||
case MapRoutePassType.仅载货可通行:
|
||||
routeColor = colorConfig.getColor('route.strokeLoaded') || get(theme, 'route.stroke-loaded') || '';
|
||||
break;
|
||||
case MapRoutePassType.禁行:
|
||||
routeColor = colorConfig.getColor('route.strokeForbidden') || get(theme, 'route.stroke-forbidden') || '';
|
||||
break;
|
||||
default:
|
||||
// 无限制路线使用无路线颜色作为默认
|
||||
routeColor = colorConfig.getColor('route.strokeNone') || get(theme, 'route.stroke-0') || '';
|
||||
break;
|
||||
}
|
||||
}
|
||||
ctx.strokeStyle = routeColor;
|
||||
// 使用配置的路线宽度
|
||||
const routeWidth = colorConfig.getRouteWidth(active);
|
||||
ctx.lineWidth = routeWidth * s;
|
||||
ctx.moveTo(x1, y1);
|
||||
switch (type) {
|
||||
case MapRouteType.直线:
|
||||
ctx.lineTo(x2, y2);
|
||||
break;
|
||||
case MapRouteType.二阶贝塞尔曲线:
|
||||
ctx.quadraticCurveTo(c1x, c1y, x2, y2);
|
||||
p1.next = { x: x1 + (2 / 3) * dx1 * s, y: y1 + (2 / 3) * dy1 * s };
|
||||
p2.prev = { x: x2 / 3 + (2 / 3) * c1x, y: y2 / 3 + (2 / 3) * c1y };
|
||||
break;
|
||||
case MapRouteType.三阶贝塞尔曲线:
|
||||
ctx.bezierCurveTo(c1x, c1y, c2x, c2y, x2, y2);
|
||||
p1.next = { x: c1x, y: c1y };
|
||||
p2.prev = { x: c2x, y: c2y };
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
if (pass === MapRoutePassType.禁行) {
|
||||
ctx.setLineDash([s * 5]);
|
||||
}
|
||||
ctx.stroke();
|
||||
ctx.beginPath();
|
||||
ctx.setLineDash([0]);
|
||||
|
||||
const { dx, dy, r } = (() => {
|
||||
switch (type) {
|
||||
case MapRouteType.直线: {
|
||||
const t = direction < 0 ? 0.55 : 0.45;
|
||||
const dx = x1 + (x2 - x1) * t;
|
||||
const dy = y1 + (y2 - y1) * t;
|
||||
const r = Math.atan2(y2 - y1, x2 - x1) + (direction > 0 ? Math.PI : 0);
|
||||
return { dx, dy, r };
|
||||
}
|
||||
case MapRouteType.二阶贝塞尔曲线: {
|
||||
const { x: dx, y: dy, t } = getBezier2Center(p1, { x: c1x, y: c1y }, p2);
|
||||
const r = getBezier2Tange(p1, { x: c1x, y: c1y }, p2, t) + (direction > 0 ? Math.PI : 0);
|
||||
return { dx, dy, r };
|
||||
}
|
||||
case MapRouteType.三阶贝塞尔曲线: {
|
||||
const { x: dx, y: dy, t } = getBezier3Center(p1, { x: c1x, y: c1y }, { x: c2x, y: c2y }, p2);
|
||||
const r = getBezier3Tange(p1, { x: c1x, y: c1y }, { x: c2x, y: c2y }, p2, t) + (direction > 0 ? Math.PI : 0);
|
||||
return { dx, dy, r };
|
||||
}
|
||||
default:
|
||||
return { dx: 0, dy: 0, r: 0 };
|
||||
}
|
||||
})();
|
||||
ctx.translate(dx, dy);
|
||||
ctx.moveTo(Math.cos(r + Math.PI / 5) * s * 10, Math.sin(r + Math.PI / 5) * s * 10);
|
||||
ctx.lineTo(0, 0);
|
||||
ctx.lineTo(Math.cos(r - Math.PI / 5) * s * 10, Math.sin(r - Math.PI / 5) * s * 10);
|
||||
ctx.stroke();
|
||||
ctx.setTransform(1, 0, 0, 1, 0, 0);
|
||||
ctx.restore();
|
||||
}
|
||||
|
||||
export function lineBezier2(_: Meta2dStore, pen: MapPen): void {
|
||||
if (pen.calculative?.worldAnchors?.length !== 2) return;
|
||||
const { c1 } = pen.route ?? {};
|
||||
const { lineWidth: s = 1 } = pen.calculative ?? {};
|
||||
const [p1, p2] = pen.calculative?.worldAnchors ?? [];
|
||||
const { x: x1 = 0, y: y1 = 0 } = p1 ?? {};
|
||||
const { x: x2 = 0, y: y2 = 0 } = p2 ?? {};
|
||||
const { x: dx = 0, y: dy = 0 } = c1 ?? {};
|
||||
pen.calculative.worldAnchors[0].next = { x: x1 + (2 / 3) * dx * s, y: y1 + (2 / 3) * dy * s };
|
||||
pen.calculative.worldAnchors[1].prev = { x: x2 / 3 + (2 / 3) * (x1 + dx * s), y: y2 / 3 + (2 / 3) * (y1 + dy * s) };
|
||||
}
|
||||
|
||||
export function lineBezier3(_: Meta2dStore, pen: MapPen): void {
|
||||
if (pen.calculative?.worldAnchors?.length !== 2) return;
|
||||
const { c1, c2 } = pen.route ?? {};
|
||||
const { lineWidth: s = 1 } = pen.calculative ?? {};
|
||||
const [p1, p2] = pen.calculative?.worldAnchors ?? [];
|
||||
const { x: x1 = 0, y: y1 = 0 } = p1 ?? {};
|
||||
const { x: x2 = 0, y: y2 = 0 } = p2 ?? {};
|
||||
const { x: dx1 = 0, y: dy1 = 0 } = c1 ?? {};
|
||||
const { x: dx2 = 0, y: dy2 = 0 } = c2 ?? {};
|
||||
pen.calculative.worldAnchors[0].next = { x: x1 + dx1 * s, y: y1 + dy1 * s };
|
||||
pen.calculative.worldAnchors[1].prev = { x: x2 + dx2 * s, y: y2 + dy2 * s };
|
||||
}
|
||||
|
||||
export function drawArea(ctx: CanvasRenderingContext2D, pen: MapPen): void {
|
||||
const theme = sTheme.editor;
|
||||
const { active, fontSize = 14, lineHeight = 1.5, fontFamily } = pen.calculative ?? {};
|
||||
const { x = 0, y = 0, width: w = 0, height: h = 0 } = pen.calculative?.worldRect ?? {};
|
||||
const { type } = pen.area ?? {};
|
||||
const { label = '', desc = '' } = pen ?? {};
|
||||
|
||||
ctx.save();
|
||||
ctx.rect(x, y, w, h);
|
||||
|
||||
// 填充颜色:优先通用,再类型,再主题
|
||||
const generalFillColor = colorConfig.getColor(`area.fill.${type}`);
|
||||
const typeFillColor = colorConfig.getColor(`area.types.${type}.fill`);
|
||||
const themeFillColor = get(theme, `area.fill-${type}`);
|
||||
const finalFillColor =
|
||||
generalFillColor || typeFillColor || themeFillColor || colorConfig.getColor('area.fill.1') || '#e6f4ff33';
|
||||
ctx.fillStyle = finalFillColor;
|
||||
ctx.fill();
|
||||
|
||||
// 边框颜色与样式
|
||||
const borderColor = type ? colorConfig.getAreaBorderColor(type) : '';
|
||||
const generalStrokeColor = colorConfig.getColor(active ? 'area.strokeActive' : `area.stroke.${type}`);
|
||||
const typeStrokeColor = colorConfig.getColor(`area.types.${type}.stroke`);
|
||||
const themeStrokeColor = get(theme, active ? 'area.strokeActive' : `area.stroke-${type}`);
|
||||
const borderWidth = type ? colorConfig.getAreaBorderWidth(type) : 1;
|
||||
const borderOpacity = type ? colorConfig.getAreaBorderOpacity(type) : 0.15;
|
||||
|
||||
ctx.lineWidth = borderWidth;
|
||||
ctx.setLineDash([]);
|
||||
let finalStrokeColor = borderColor || generalStrokeColor || typeStrokeColor || themeStrokeColor || '';
|
||||
if (borderOpacity < 1 && finalStrokeColor.startsWith('#')) {
|
||||
const alpha = Math.round(borderOpacity * 255).toString(16).padStart(2, '0');
|
||||
finalStrokeColor = finalStrokeColor + alpha;
|
||||
}
|
||||
if (!finalStrokeColor) {
|
||||
finalStrokeColor = colorConfig.getColor('area.strokeActive') || '#8C8C8C';
|
||||
}
|
||||
ctx.strokeStyle = finalStrokeColor;
|
||||
ctx.stroke();
|
||||
|
||||
// 描述区渲染文字
|
||||
if (type === MapAreaType.描述区 && desc) {
|
||||
ctx.fillStyle = colorConfig.getColor('common.color') || (get(theme, 'color') ?? '');
|
||||
|
||||
let descFontSize = Math.min(w / 6, h / 4, 200);
|
||||
let lines: string[] = [];
|
||||
while (descFontSize > 1) {
|
||||
ctx.font = `${descFontSize}px ${fontFamily}`;
|
||||
const maxCharsPerLine = Math.floor(w / (descFontSize * 0.8));
|
||||
if (maxCharsPerLine < 1) {
|
||||
descFontSize = Math.floor(descFontSize * 0.9);
|
||||
continue;
|
||||
}
|
||||
lines = [];
|
||||
for (let i = 0; i < desc.length; i += maxCharsPerLine) {
|
||||
lines.push(desc.slice(i, i + maxCharsPerLine));
|
||||
}
|
||||
const textMetrics = ctx.measureText('测试文字');
|
||||
const lh = (textMetrics.actualBoundingBoxAscent + textMetrics.actualBoundingBoxDescent) || descFontSize;
|
||||
const totalHeight = lines.length * lh * 1.1;
|
||||
if (totalHeight <= h * 0.9) break;
|
||||
descFontSize = Math.floor(descFontSize * 0.9);
|
||||
}
|
||||
|
||||
ctx.font = `${descFontSize}px ${fontFamily}`;
|
||||
ctx.textAlign = 'center';
|
||||
ctx.textBaseline = 'top';
|
||||
const textMetrics = ctx.measureText('测试文字');
|
||||
const lh = (textMetrics.actualBoundingBoxAscent + textMetrics.actualBoundingBoxDescent) || descFontSize;
|
||||
const totalHeight = lines.length * lh * 1.1;
|
||||
const startY = y + h / 2 - totalHeight / 2;
|
||||
lines.forEach((line, index) => {
|
||||
ctx.fillText(line, x + w / 2, startY + index * lh * 1.1);
|
||||
});
|
||||
} else if (type !== MapAreaType.描述区 && label) {
|
||||
// 非描述区显示标签
|
||||
ctx.fillStyle = colorConfig.getColor('common.color') || (get(theme, 'color') ?? '');
|
||||
ctx.font = `${fontSize}px/${lineHeight} ${fontFamily}`;
|
||||
ctx.textAlign = 'center';
|
||||
ctx.textBaseline = 'top';
|
||||
ctx.fillText(label, x + w / 2, y - fontSize * lineHeight);
|
||||
}
|
||||
// 门区域:右上角角标显示门状态
|
||||
if ((type as any) === DOOR_AREA_TYPE) {
|
||||
const isConnected = (pen.area as any)?.isConnected;
|
||||
const deviceStatus = (pen.area as any)?.deviceStatus;
|
||||
const r2 = Math.max(6, Math.min(12, Math.min(w, h) * 0.08));
|
||||
const cx2 = x + w - r2 - 6;
|
||||
const cy2 = y + r2 + 6;
|
||||
ctx.beginPath();
|
||||
ctx.arc(cx2, cy2, r2, 0, Math.PI * 2);
|
||||
if (isConnected === false) {
|
||||
ctx.fillStyle = colorConfig.getColor('autoDoor.strokeClosed') || '#fe5a5ae0';
|
||||
} else if (deviceStatus === 1) {
|
||||
ctx.fillStyle = colorConfig.getColor('autoDoor.fillOpen') || '#1890FF';
|
||||
} else {
|
||||
ctx.fillStyle = colorConfig.getColor('autoDoor.fillClosed') || '#cddc39';
|
||||
}
|
||||
ctx.fill();
|
||||
}
|
||||
|
||||
|
||||
ctx.restore();
|
||||
}
|
||||
|
||||
// 辅助函数仅在本模块内部使用
|
||||
function getBezier2Center(p1: Point, c1: Point, p2: Point): Point & { t: number } {
|
||||
const fn = (t: number) => {
|
||||
const x = (1 - t) ** 2 * p1.x + 2 * (1 - t) * t * c1.x + t ** 2 * p2.x;
|
||||
const y = (1 - t) ** 2 * p1.y + 2 * (1 - t) * t * c1.y + t ** 2 * p2.y;
|
||||
return { x, y };
|
||||
};
|
||||
return calcBezierCenter(fn);
|
||||
}
|
||||
|
||||
function getBezier2Tange(p1: Point, c1: Point, p2: Point, t: number): number {
|
||||
const dx = 2 * (1 - t) * (c1.x - p1.x) + 2 * t * (p2.x - c1.x);
|
||||
const dy = 2 * (1 - t) * (c1.y - p1.y) + 2 * t * (p2.y - c1.y);
|
||||
return Math.atan2(dy, dx);
|
||||
}
|
||||
|
||||
function getBezier3Center(p1: Point, c1: Point, c2: Point, p2: Point): Point & { t: number } {
|
||||
const fn = (t: number) => {
|
||||
const x = (1 - t) ** 3 * p1.x + 3 * (1 - t) ** 2 * t * c1.x + 3 * (1 - t) * t ** 2 * c2.x + t ** 3 * p2.x;
|
||||
const y = (1 - t) ** 3 * p1.y + 3 * (1 - t) ** 2 * t * c1.y + 3 * (1 - t) * t ** 2 * c2.y + t ** 3 * p2.y;
|
||||
return { x, y };
|
||||
};
|
||||
return calcBezierCenter(fn);
|
||||
}
|
||||
|
||||
function getBezier3Tange(p1: Point, c1: Point, c2: Point, p2: Point, t: number): number {
|
||||
const t1 = 3 * Math.pow(1 - t, 2);
|
||||
const t2 = 6 * (1 - t) * t;
|
||||
const t3 = 3 * Math.pow(t, 2);
|
||||
|
||||
const dx = t1 * (c1.x - p1.x) + t2 * (c2.x - c1.x) + t3 * (p2.x - c2.x);
|
||||
const dy = t1 * (c1.y - p1.y) + t2 * (c2.y - c1.y) + t3 * (p2.y - c2.y);
|
||||
return Math.atan2(dy, dx);
|
||||
}
|
||||
|
||||
function calcBezierCenter(bezierFn: (t: number) => Point): Point & { t: number } {
|
||||
const count = 23;
|
||||
|
||||
let length = 0;
|
||||
let temp = bezierFn(0);
|
||||
const samples = Array.from({ length: count }, (_, i) => {
|
||||
const t = (i + 1) / count;
|
||||
const point = bezierFn(t);
|
||||
const dx = point.x - temp.x;
|
||||
const dy = point.y - temp.y;
|
||||
length += Math.sqrt(dx * dx + dy * dy);
|
||||
temp = point;
|
||||
return { ...point, t };
|
||||
});
|
||||
|
||||
const target = length * 0.45;
|
||||
let accumulated = 0;
|
||||
for (let i = 0; i < samples.length - 1; i++) {
|
||||
const p1 = samples[i];
|
||||
const p2 = samples[i + 1];
|
||||
const segment = Math.sqrt((p2.x - p1.x) ** 2 + (p2.y - p1.y) ** 2);
|
||||
if (accumulated + segment >= target) {
|
||||
const ratio = (target - accumulated) / segment;
|
||||
return {
|
||||
x: p1.x + (p2.x - p1.x) * ratio,
|
||||
y: p1.y + (p2.y - p1.y) * ratio,
|
||||
t: p1.t + ratio * (p2.t - p1.t),
|
||||
};
|
||||
}
|
||||
accumulated += segment;
|
||||
}
|
||||
return samples[samples.length - 1];
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
import { MapAreaType, type MapPen, MapPointType, MapRoutePassType, MapRouteType, type Point } from '@api/map';
|
||||
import { DOOR_AREA_TYPE } from '@api/map/door-area';
|
||||
import sTheme from '@core/theme.service';
|
||||
import { type Meta2dStore } from '@meta2d/core';
|
||||
import { get } from 'lodash-es';
|
||||
|
||||
import doorClosedUrl from '../../assets/icons/png/guanmen.png';
|
||||
import doorOpenUrl from '../../assets/icons/png/weixinkaimen.png';
|
||||
import colorConfig from '../color/color-config.service';
|
||||
const __doorImgOpen = new Image();
|
||||
__doorImgOpen.src = (doorOpenUrl as unknown as string) || '';
|
||||
const __doorImgClosed = new Image();
|
||||
__doorImgClosed.src = (doorClosedUrl as unknown as string) || '';
|
||||
|
||||
export function drawPoint(ctx: CanvasRenderingContext2D, pen: MapPen): void {
|
||||
const theme = sTheme.editor;
|
||||
const { active, iconSize: r = 0, fontSize = 14, lineHeight = 1.5, fontFamily } = pen.calculative ?? {};
|
||||
const { x = 0, y = 0, width: w = 0, height: h = 0 } = pen.calculative?.worldRect ?? {};
|
||||
const { type, isConnected, deviceStatus, active: pointActive } = pen.point ?? {};
|
||||
const { label = '', statusStyle } = pen ?? {};
|
||||
|
||||
ctx.save();
|
||||
|
||||
// 自动门点:根据连接与开关状态绘制矩形光圈(无边框)
|
||||
if (type === MapPointType.自动门点 && pointActive) {
|
||||
// 让光圈随点位尺寸等比缩放,避免缩放画布时视觉上变大
|
||||
const base = Math.min(w, h);
|
||||
const padding = Math.max(2, Math.min(10, base * 0.2));
|
||||
const rx = x - padding;
|
||||
const ry = y - padding;
|
||||
const rw = w + padding * 2;
|
||||
const rh = h + padding * 2;
|
||||
|
||||
// 使用与点位相同的圆角半径,使观感统一
|
||||
ctx.beginPath();
|
||||
ctx.roundRect(rx, ry, rw, rh, r);
|
||||
|
||||
if (isConnected === false) {
|
||||
// 未连接:深红色实心,不描边
|
||||
ctx.fillStyle = colorConfig.getColor('autoDoor.strokeClosed') || '#fe5a5ae0';
|
||||
} else {
|
||||
// 已连接:根据门状态显示颜色(0=关门-浅红,1=开门-蓝色)
|
||||
if (deviceStatus === 0) {
|
||||
ctx.fillStyle = colorConfig.getColor('autoDoor.fillClosed') || '#cddc39';
|
||||
} else {
|
||||
ctx.fillStyle = colorConfig.getColor('autoDoor.fillOpen') || '#1890FF';
|
||||
}
|
||||
}
|
||||
ctx.fill();
|
||||
// 重置路径,避免后续对点位边框的 stroke 影响到光圈路径
|
||||
ctx.beginPath();
|
||||
}
|
||||
|
||||
switch (type) {
|
||||
case MapPointType.普通点:
|
||||
case MapPointType.等待点:
|
||||
case MapPointType.避让点:
|
||||
case MapPointType.临时避让点:
|
||||
case MapPointType.库区点:
|
||||
case MapPointType.不可避让点: {
|
||||
ctx.beginPath();
|
||||
ctx.moveTo(x + w / 2 - r, y + r);
|
||||
ctx.arcTo(x + w / 2, y, x + w - r, y + h / 2 - r, r);
|
||||
ctx.arcTo(x + w, y + h / 2, x + w / 2 + r, y + h - r, r);
|
||||
ctx.arcTo(x + w / 2, y + h, x + r, y + h / 2 + r, r);
|
||||
ctx.arcTo(x, y + h / 2, x + r, y + h / 2 - r, r);
|
||||
ctx.closePath();
|
||||
// 优先使用小点位专用颜色,如果没有则使用类型专用颜色
|
||||
const smallGeneralColor = colorConfig.getColor(`point.small.fill.${type}`);
|
||||
const smallTypeColor = colorConfig.getColor(`point.types.${type}.fill`);
|
||||
const smallThemeColor = get(theme, `point-s.fill-${type}`);
|
||||
const finalColor = smallGeneralColor || smallTypeColor || smallThemeColor || '';
|
||||
|
||||
ctx.fillStyle = finalColor;
|
||||
ctx.fill();
|
||||
|
||||
const smallTypeStrokeColor = colorConfig.getColor(`point.types.${type}.stroke`);
|
||||
const smallGeneralStrokeColor = colorConfig.getColor(active ? 'point.small.strokeActive' : 'point.small.stroke');
|
||||
const smallThemeStrokeColor = get(theme, active ? 'point-s.strokeActive' : 'point-s.stroke');
|
||||
ctx.strokeStyle = smallTypeStrokeColor || smallGeneralStrokeColor || smallThemeStrokeColor || '';
|
||||
if (type === MapPointType.临时避让点) {
|
||||
ctx.lineCap = 'round';
|
||||
ctx.beginPath();
|
||||
ctx.moveTo(x + 0.66 * r, y + h / 2 - 0.66 * r);
|
||||
ctx.lineTo(x + r, y + h / 2 - r);
|
||||
ctx.moveTo(x + w / 2 - 0.66 * r, y + 0.66 * r);
|
||||
ctx.lineTo(x + w / 2 - r, y + r);
|
||||
ctx.moveTo(x + w / 2 + 0.66 * r, y + 0.66 * r);
|
||||
ctx.lineTo(x + w / 2 + r, y + r);
|
||||
ctx.moveTo(x + w - 0.66 * r, y + h / 2 - 0.66 * r);
|
||||
ctx.lineTo(x + w - r, y + h / 2 - r);
|
||||
ctx.moveTo(x + w - 0.66 * r, y + h / 2 + 0.66 * r);
|
||||
ctx.lineTo(x + w - r, y + h / 2 + r);
|
||||
ctx.moveTo(x + w / 2 + 0.66 * r, y + h - 0.66 * r);
|
||||
ctx.lineTo(x + w / 2 + r, y + h - r);
|
||||
ctx.moveTo(x + w / 2 - 0.66 * r, y + h - 0.66 * r);
|
||||
ctx.lineTo(x + w / 2 - r, y + h - r);
|
||||
ctx.moveTo(x + 0.66 * r, y + h / 2 + 0.66 * r);
|
||||
ctx.lineTo(x + r, y + h / 2 + r);
|
||||
}
|
||||
ctx.stroke();
|
||||
break;
|
||||
}
|
||||
case MapPointType.电梯点:
|
||||
case MapPointType.自动门点:
|
||||
case MapPointType.充电点:
|
||||
case MapPointType.停靠点:
|
||||
case MapPointType.动作点:
|
||||
case MapPointType.禁行点: {
|
||||
ctx.roundRect(x, y, w, h, r);
|
||||
|
||||
// 优先使用类型专用颜色,如果没有则使用通用颜色
|
||||
const largeTypeStrokeColor = colorConfig.getColor(`point.types.${type}.stroke`);
|
||||
const largeGeneralStrokeColor = colorConfig.getColor(active ? 'point.large.strokeActive' : 'point.large.stroke');
|
||||
const largeThemeStrokeColor = get(theme, active ? 'point-l.strokeActive' : 'point-l.stroke');
|
||||
ctx.strokeStyle = statusStyle ?? (largeTypeStrokeColor || largeGeneralStrokeColor || largeThemeStrokeColor || '');
|
||||
ctx.stroke();
|
||||
break;
|
||||
}
|
||||
default:
|
||||
break;
|
||||
}
|
||||
ctx.fillStyle = colorConfig.getColor('common.color') || (get(theme, 'color') ?? '');
|
||||
ctx.font = `${fontSize}px/${lineHeight} ${fontFamily}`;
|
||||
ctx.textAlign = 'center';
|
||||
ctx.textBaseline = 'top';
|
||||
ctx.fillText(label, x + w / 2, y - fontSize * lineHeight);
|
||||
|
||||
ctx.restore();
|
||||
}
|
||||
|
||||
export function anchorPoint(pen: MapPen): void {
|
||||
pen.anchors = [{ penId: pen.id, id: '0', x: 0.5, y: 0.5 }];
|
||||
}
|
||||
|
||||
export function drawLine(ctx: CanvasRenderingContext2D, pen: MapPen): void {
|
||||
const theme = sTheme.editor;
|
||||
const { active, lineWidth: s = 1 } = pen.calculative ?? {};
|
||||
const [p1, p2] = pen.calculative?.worldAnchors ?? [];
|
||||
const { x: x1 = 0, y: y1 = 0 } = p1 ?? {};
|
||||
const { x: x2 = 0, y: y2 = 0 } = p2 ?? {};
|
||||
const { type, direction = 1, pass = 0, c1, c2 } = pen.route ?? {};
|
||||
const { x: dx1 = 0, y: dy1 = 0 } = c1 ?? {};
|
||||
const { x: dx2 = 0, y: dy2 = 0 } = c2 ?? {};
|
||||
const [c1x, c1y] = [x1 + dx1 * s, y1 + dy1 * s];
|
||||
const [c2x, c2y] = [x2 + dx2 * s, y2 + dy2 * s];
|
||||
|
||||
ctx.save();
|
||||
ctx.beginPath();
|
||||
// 根据路线通行类型获取颜色
|
||||
let routeColor = '';
|
||||
if (active) {
|
||||
routeColor = colorConfig.getColor('route.strokeActive') || get(theme, 'route.strokeActive') || '';
|
||||
} else {
|
||||
// 根据通行类型选择颜色
|
||||
switch (pass) {
|
||||
case MapRoutePassType.无:
|
||||
routeColor = colorConfig.getColor('route.strokeNone') || get(theme, 'route.stroke-0') || '';
|
||||
break;
|
||||
case MapRoutePassType.仅空载可通行:
|
||||
routeColor = colorConfig.getColor('route.strokeEmpty') || get(theme, 'route.stroke-empty') || '';
|
||||
break;
|
||||
case MapRoutePassType.仅载货可通行:
|
||||
routeColor = colorConfig.getColor('route.strokeLoaded') || get(theme, 'route.stroke-loaded') || '';
|
||||
break;
|
||||
case MapRoutePassType.禁行:
|
||||
routeColor = colorConfig.getColor('route.strokeForbidden') || get(theme, 'route.stroke-forbidden') || '';
|
||||
break;
|
||||
default:
|
||||
// 无限制路线使用无路线颜色作为默认
|
||||
routeColor = colorConfig.getColor('route.strokeNone') || get(theme, 'route.stroke-0') || '';
|
||||
break;
|
||||
}
|
||||
}
|
||||
ctx.strokeStyle = routeColor;
|
||||
// 使用配置的路线宽度
|
||||
const routeWidth = colorConfig.getRouteWidth(active);
|
||||
ctx.lineWidth = routeWidth * s;
|
||||
ctx.moveTo(x1, y1);
|
||||
switch (type) {
|
||||
case MapRouteType.直线:
|
||||
ctx.lineTo(x2, y2);
|
||||
break;
|
||||
case MapRouteType.二阶贝塞尔曲线:
|
||||
ctx.quadraticCurveTo(c1x, c1y, x2, y2);
|
||||
p1.next = { x: x1 + (2 / 3) * dx1 * s, y: y1 + (2 / 3) * dy1 * s };
|
||||
p2.prev = { x: x2 / 3 + (2 / 3) * c1x, y: y2 / 3 + (2 / 3) * c1y };
|
||||
break;
|
||||
case MapRouteType.三阶贝塞尔曲线:
|
||||
ctx.bezierCurveTo(c1x, c1y, c2x, c2y, x2, y2);
|
||||
p1.next = { x: c1x, y: c1y };
|
||||
p2.prev = { x: c2x, y: c2y };
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
if (pass === MapRoutePassType.禁行) {
|
||||
ctx.setLineDash([s * 5]);
|
||||
}
|
||||
ctx.stroke();
|
||||
ctx.beginPath();
|
||||
ctx.setLineDash([0]);
|
||||
|
||||
const { dx, dy, r } = (() => {
|
||||
switch (type) {
|
||||
case MapRouteType.直线: {
|
||||
const t = direction < 0 ? 0.55 : 0.45;
|
||||
const dx = x1 + (x2 - x1) * t;
|
||||
const dy = y1 + (y2 - y1) * t;
|
||||
const r = Math.atan2(y2 - y1, x2 - x1) + (direction > 0 ? Math.PI : 0);
|
||||
return { dx, dy, r };
|
||||
}
|
||||
case MapRouteType.二阶贝塞尔曲线: {
|
||||
const { x: dx, y: dy, t } = getBezier2Center(p1, { x: c1x, y: c1y }, p2);
|
||||
const r = getBezier2Tange(p1, { x: c1x, y: c1y }, p2, t) + (direction > 0 ? Math.PI : 0);
|
||||
return { dx, dy, r };
|
||||
}
|
||||
case MapRouteType.三阶贝塞尔曲线: {
|
||||
const { x: dx, y: dy, t } = getBezier3Center(p1, { x: c1x, y: c1y }, { x: c2x, y: c2y }, p2);
|
||||
const r = getBezier3Tange(p1, { x: c1x, y: c1y }, { x: c2x, y: c2y }, p2, t) + (direction > 0 ? Math.PI : 0);
|
||||
return { dx, dy, r };
|
||||
}
|
||||
default:
|
||||
return { dx: 0, dy: 0, r: 0 };
|
||||
}
|
||||
})();
|
||||
ctx.translate(dx, dy);
|
||||
ctx.moveTo(Math.cos(r + Math.PI / 5) * s * 10, Math.sin(r + Math.PI / 5) * s * 10);
|
||||
ctx.lineTo(0, 0);
|
||||
ctx.lineTo(Math.cos(r - Math.PI / 5) * s * 10, Math.sin(r - Math.PI / 5) * s * 10);
|
||||
ctx.stroke();
|
||||
ctx.setTransform(1, 0, 0, 1, 0, 0);
|
||||
ctx.restore();
|
||||
}
|
||||
|
||||
export function lineBezier2(_: Meta2dStore, pen: MapPen): void {
|
||||
if (pen.calculative?.worldAnchors?.length !== 2) return;
|
||||
const { c1 } = pen.route ?? {};
|
||||
const { lineWidth: s = 1 } = pen.calculative ?? {};
|
||||
const [p1, p2] = pen.calculative?.worldAnchors ?? [];
|
||||
const { x: x1 = 0, y: y1 = 0 } = p1 ?? {};
|
||||
const { x: x2 = 0, y: y2 = 0 } = p2 ?? {};
|
||||
const { x: dx = 0, y: dy = 0 } = c1 ?? {};
|
||||
pen.calculative.worldAnchors[0].next = { x: x1 + (2 / 3) * dx * s, y: y1 + (2 / 3) * dy * s };
|
||||
pen.calculative.worldAnchors[1].prev = { x: x2 / 3 + (2 / 3) * (x1 + dx * s), y: y2 / 3 + (2 / 3) * (y1 + dy * s) };
|
||||
}
|
||||
|
||||
export function lineBezier3(_: Meta2dStore, pen: MapPen): void {
|
||||
if (pen.calculative?.worldAnchors?.length !== 2) return;
|
||||
const { c1, c2 } = pen.route ?? {};
|
||||
const { lineWidth: s = 1 } = pen.calculative ?? {};
|
||||
const [p1, p2] = pen.calculative?.worldAnchors ?? [];
|
||||
const { x: x1 = 0, y: y1 = 0 } = p1 ?? {};
|
||||
const { x: x2 = 0, y: y2 = 0 } = p2 ?? {};
|
||||
const { x: dx1 = 0, y: dy1 = 0 } = c1 ?? {};
|
||||
const { x: dx2 = 0, y: dy2 = 0 } = c2 ?? {};
|
||||
pen.calculative.worldAnchors[0].next = { x: x1 + dx1 * s, y: y1 + dy1 * s };
|
||||
pen.calculative.worldAnchors[1].prev = { x: x2 + dx2 * s, y: y2 + dy2 * s };
|
||||
}
|
||||
|
||||
export function drawArea(ctx: CanvasRenderingContext2D, pen: MapPen): void {
|
||||
const theme = sTheme.editor;
|
||||
const { active, fontSize = 14, lineHeight = 1.5, fontFamily } = pen.calculative ?? {};
|
||||
const { x = 0, y = 0, width: w = 0, height: h = 0 } = pen.calculative?.worldRect ?? {};
|
||||
const { type } = pen.area ?? {};
|
||||
const { label = '', desc = '' } = pen ?? {};
|
||||
|
||||
ctx.save();
|
||||
ctx.rect(x, y, w, h);
|
||||
|
||||
// 填充颜色:优先通用,再类型,再主题
|
||||
const generalFillColor = colorConfig.getColor(`area.fill.${type}`);
|
||||
const typeFillColor = colorConfig.getColor(`area.types.${type}.fill`);
|
||||
const themeFillColor = get(theme, `area.fill-${type}`);
|
||||
const finalFillColor =
|
||||
generalFillColor || typeFillColor || themeFillColor || colorConfig.getColor('area.fill.1') || '#e6f4ff33';
|
||||
ctx.fillStyle = finalFillColor;
|
||||
ctx.fill();
|
||||
|
||||
// 边框颜色与样式
|
||||
const borderColor = type ? colorConfig.getAreaBorderColor(type) : '';
|
||||
const generalStrokeColor = colorConfig.getColor(active ? 'area.strokeActive' : `area.stroke.${type}`);
|
||||
const typeStrokeColor = colorConfig.getColor(`area.types.${type}.stroke`);
|
||||
const themeStrokeColor = get(theme, active ? 'area.strokeActive' : `area.stroke-${type}`);
|
||||
const borderWidth = type ? colorConfig.getAreaBorderWidth(type) : 1;
|
||||
const borderOpacity = type ? colorConfig.getAreaBorderOpacity(type) : 0.15;
|
||||
|
||||
ctx.lineWidth = borderWidth;
|
||||
ctx.setLineDash([]);
|
||||
let finalStrokeColor = borderColor || generalStrokeColor || typeStrokeColor || themeStrokeColor || '';
|
||||
if (borderOpacity < 1 && finalStrokeColor.startsWith('#')) {
|
||||
const alpha = Math.round(borderOpacity * 255)
|
||||
.toString(16)
|
||||
.padStart(2, '0');
|
||||
finalStrokeColor = finalStrokeColor + alpha;
|
||||
}
|
||||
if (!finalStrokeColor) {
|
||||
finalStrokeColor = colorConfig.getColor('area.strokeActive') || '#8C8C8C';
|
||||
}
|
||||
// 门区域断连:加粗、红色(不闪烁)
|
||||
if ((type as any) === DOOR_AREA_TYPE) {
|
||||
const isConnected = (pen.area as any)?.isConnected;
|
||||
if (isConnected === false) {
|
||||
finalStrokeColor = '#ff4d4f';
|
||||
ctx.lineWidth = Math.max(borderWidth * 2, 4);
|
||||
ctx.setLineDash([]);
|
||||
}
|
||||
}
|
||||
ctx.strokeStyle = finalStrokeColor;
|
||||
ctx.stroke();
|
||||
|
||||
// 门区域图标背景:根据设备连接与开关状态绘制淡化图标
|
||||
if ((type as any) === DOOR_AREA_TYPE) {
|
||||
const isConnected = (pen.area as any)?.isConnected;
|
||||
const deviceStatus = (pen.area as any)?.deviceStatus;
|
||||
const img = isConnected === false ? __doorImgClosed : deviceStatus === 1 ? __doorImgOpen : __doorImgClosed;
|
||||
if (img && img.complete) {
|
||||
const padding = Math.max(6, Math.min(20, Math.min(w, h) * 0.08));
|
||||
const availW = Math.max(0, w - padding * 2);
|
||||
const availH = Math.max(0, h - padding * 2);
|
||||
const ratio = img.naturalWidth > 0 && img.naturalHeight > 0 ? img.naturalWidth / img.naturalHeight : 1;
|
||||
let drawW = availW;
|
||||
let drawH = drawW / ratio;
|
||||
if (drawH > availH) {
|
||||
drawH = availH;
|
||||
drawW = drawH * ratio;
|
||||
}
|
||||
const dx = x + (w - drawW) / 2;
|
||||
const dy = y + (h - drawH) / 2;
|
||||
// 按原图不透明绘制
|
||||
ctx.drawImage(img, dx, dy, drawW, drawH);
|
||||
}
|
||||
}
|
||||
|
||||
// 描述区渲染文字
|
||||
if (type === MapAreaType.描述区 && desc) {
|
||||
ctx.fillStyle = colorConfig.getColor('common.color') || (get(theme, 'color') ?? '');
|
||||
|
||||
let descFontSize = Math.min(w / 6, h / 4, 200);
|
||||
let lines: string[] = [];
|
||||
while (descFontSize > 1) {
|
||||
ctx.font = `${descFontSize}px ${fontFamily}`;
|
||||
const maxCharsPerLine = Math.floor(w / (descFontSize * 0.8));
|
||||
if (maxCharsPerLine < 1) {
|
||||
descFontSize = Math.floor(descFontSize * 0.9);
|
||||
continue;
|
||||
}
|
||||
lines = [];
|
||||
for (let i = 0; i < desc.length; i += maxCharsPerLine) {
|
||||
lines.push(desc.slice(i, i + maxCharsPerLine));
|
||||
}
|
||||
const textMetrics = ctx.measureText('测试文字');
|
||||
const lh = textMetrics.actualBoundingBoxAscent + textMetrics.actualBoundingBoxDescent || descFontSize;
|
||||
const totalHeight = lines.length * lh * 1.1;
|
||||
if (totalHeight <= h * 0.9) break;
|
||||
descFontSize = Math.floor(descFontSize * 0.9);
|
||||
}
|
||||
|
||||
ctx.font = `${descFontSize}px ${fontFamily}`;
|
||||
ctx.textAlign = 'center';
|
||||
ctx.textBaseline = 'top';
|
||||
const textMetrics = ctx.measureText('测试文字');
|
||||
const lh = textMetrics.actualBoundingBoxAscent + textMetrics.actualBoundingBoxDescent || descFontSize;
|
||||
const totalHeight = lines.length * lh * 1.1;
|
||||
const startY = y + h / 2 - totalHeight / 2;
|
||||
lines.forEach((line, index) => {
|
||||
ctx.fillText(line, x + w / 2, startY + index * lh * 1.1);
|
||||
});
|
||||
} else if (type !== MapAreaType.描述区 && label) {
|
||||
// 非描述区显示标签
|
||||
ctx.fillStyle = colorConfig.getColor('common.color') || (get(theme, 'color') ?? '');
|
||||
ctx.font = `${fontSize}px/${lineHeight} ${fontFamily}`;
|
||||
ctx.textAlign = 'center';
|
||||
ctx.textBaseline = 'top';
|
||||
ctx.fillText(label, x + w / 2, y - fontSize * lineHeight);
|
||||
}
|
||||
// 门区域:右上角角标显示门状态
|
||||
// (已移除门区域右上角状态圆点,状态由背景图标与边框闪烁体现)
|
||||
// 保留占位,避免大范围改动影响后续合并
|
||||
|
||||
ctx.restore();
|
||||
}
|
||||
|
||||
// 辅助函数仅在本模块内部使用
|
||||
function getBezier2Center(p1: Point, c1: Point, p2: Point): Point & { t: number } {
|
||||
const fn = (t: number) => {
|
||||
const x = (1 - t) ** 2 * p1.x + 2 * (1 - t) * t * c1.x + t ** 2 * p2.x;
|
||||
const y = (1 - t) ** 2 * p1.y + 2 * (1 - t) * t * c1.y + t ** 2 * p2.y;
|
||||
return { x, y };
|
||||
};
|
||||
return calcBezierCenter(fn);
|
||||
}
|
||||
|
||||
function getBezier2Tange(p1: Point, c1: Point, p2: Point, t: number): number {
|
||||
const dx = 2 * (1 - t) * (c1.x - p1.x) + 2 * t * (p2.x - c1.x);
|
||||
const dy = 2 * (1 - t) * (c1.y - p1.y) + 2 * t * (p2.y - c1.y);
|
||||
return Math.atan2(dy, dx);
|
||||
}
|
||||
|
||||
function getBezier3Center(p1: Point, c1: Point, c2: Point, p2: Point): Point & { t: number } {
|
||||
const fn = (t: number) => {
|
||||
const x = (1 - t) ** 3 * p1.x + 3 * (1 - t) ** 2 * t * c1.x + 3 * (1 - t) * t ** 2 * c2.x + t ** 3 * p2.x;
|
||||
const y = (1 - t) ** 3 * p1.y + 3 * (1 - t) ** 2 * t * c1.y + 3 * (1 - t) * t ** 2 * c2.y + t ** 3 * p2.y;
|
||||
return { x, y };
|
||||
};
|
||||
return calcBezierCenter(fn);
|
||||
}
|
||||
|
||||
function getBezier3Tange(p1: Point, c1: Point, c2: Point, p2: Point, t: number): number {
|
||||
const t1 = 3 * Math.pow(1 - t, 2);
|
||||
const t2 = 6 * (1 - t) * t;
|
||||
const t3 = 3 * Math.pow(t, 2);
|
||||
|
||||
const dx = t1 * (c1.x - p1.x) + t2 * (c2.x - c1.x) + t3 * (p2.x - c2.x);
|
||||
const dy = t1 * (c1.y - p1.y) + t2 * (c2.y - c1.y) + t3 * (p2.y - c2.y);
|
||||
return Math.atan2(dy, dx);
|
||||
}
|
||||
|
||||
function calcBezierCenter(bezierFn: (t: number) => Point): Point & { t: number } {
|
||||
const count = 23;
|
||||
|
||||
let length = 0;
|
||||
let temp = bezierFn(0);
|
||||
const samples = Array.from({ length: count }, (_, i) => {
|
||||
const t = (i + 1) / count;
|
||||
const point = bezierFn(t);
|
||||
const dx = point.x - temp.x;
|
||||
const dy = point.y - temp.y;
|
||||
length += Math.sqrt(dx * dx + dy * dy);
|
||||
temp = point;
|
||||
return { ...point, t };
|
||||
});
|
||||
|
||||
const target = length * 0.45;
|
||||
let accumulated = 0;
|
||||
for (let i = 0; i < samples.length - 1; i++) {
|
||||
const p1 = samples[i];
|
||||
const p2 = samples[i + 1];
|
||||
const segment = Math.sqrt((p2.x - p1.x) ** 2 + (p2.y - p1.y) ** 2);
|
||||
if (accumulated + segment >= target) {
|
||||
const ratio = (target - accumulated) / segment;
|
||||
return {
|
||||
x: p1.x + (p2.x - p1.x) * ratio,
|
||||
y: p1.y + (p2.y - p1.y) * ratio,
|
||||
t: p1.t + ratio * (p2.t - p1.t),
|
||||
};
|
||||
}
|
||||
accumulated += segment;
|
||||
}
|
||||
return samples[samples.length - 1];
|
||||
}
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user