diff --git a/src/services/editor.service.ts b/src/services/editor.service.ts index 5aec9ed..1b14ae9 100644 --- a/src/services/editor.service.ts +++ b/src/services/editor.service.ts @@ -21,7 +21,7 @@ import type { StandardSceneRoute, } from '@api/scene'; import sTheme from '@core/theme.service'; -import { LockState, Meta2d, type Meta2dStore, type Pen } from '@meta2d/core'; +import { LockState, Meta2d, type Pen } from '@meta2d/core'; import { useObservable } from '@vueuse/rxjs'; import { clone, get, isEmpty, isNil, isString, pick, } from 'lodash-es'; import { debounceTime, filter, map, Subject, switchMap } from 'rxjs'; @@ -37,9 +37,10 @@ import { drawStorageMore, } from './draw/storage-location-drawer'; import { AreaManager } from './editor/area-manager.service'; +import { anchorPoint, drawArea, drawLine, drawPoint, lineBezier2, lineBezier3 } from './editor/editor-drawers'; +import { drawRobot,EditorRobotService } from './editor/editor-robot.service'; import { PointManager } from './editor/point-manager.service'; import { RouteManager } from './editor/route-manager.service'; -import { drawRobot,EditorRobotService } from './editor/editor-robot.service'; import { LayerManagerService } from './layer-manager.service'; import { createStorageLocationUpdater,StorageLocationService } from './storage-location.service'; import { AutoStorageGenerator } from './utils/auto-storage-generator'; @@ -1474,474 +1475,5 @@ export class EditorService extends Meta2d { } //#region 自定义绘制函数 -/** - * 绘制点位的自定义函数 - * @param ctx Canvas 2D绘制上下文 - * @param pen 点位图形对象 - */ -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); - - // 库位2x3栅格:动作点在画布上直接绘制(静态数据) - // 注意:库位现在通过动态pen对象渲染,不再使用静态绘制 - // if (type === MapPointType.动作点) { - // drawStorageGrid(ctx, pen, { - // fontFamily, - // stateResolver: (penId: string, layerName: string) => { - // const inner = storageStateMap.get(penId); - // if (!inner) { - // return {}; - // } - // const state = inner.get(layerName); - - // return state ?? {}; - // }, - // }); - // } - ctx.restore(); -} -/** - * 设置点位的连接锚点 - * @param pen 点位图形对象 - */ -function anchorPoint(pen: MapPen): void { - pen.anchors = [ - { penId: pen.id, id: '0', x: 0.5, y: 0.5 }, - // { penId: pen.id, id: 't', x: 0.5, y: 0 }, - // { penId: pen.id, id: 'b', x: 0.5, y: 1 }, - // { penId: pen.id, id: 'l', x: 0, y: 0.5 }, - // { penId: pen.id, id: 'r', x: 1, y: 0.5 }, - ]; -} - -/** - * 绘制路线的自定义函数 - * @param ctx Canvas 2D绘制上下文 - * @param pen 路线图形对象 - */ -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(); -} -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) }; -} -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 }; -} - -/** - * 绘制区域的自定义函数 - * @param ctx Canvas 2D绘制上下文 - * @param pen 区域图形对象 - */ -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 || ''; - - - 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; - } - - 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 lineHeight = textMetrics.actualBoundingBoxAscent + textMetrics.actualBoundingBoxDescent; - const totalHeight = lines.length * lineHeight * 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 lineHeight = textMetrics.actualBoundingBoxAscent + textMetrics.actualBoundingBoxDescent; - const totalHeight = lines.length * lineHeight * 1.1; - const startY = y + h / 2 - totalHeight / 2; - - lines.forEach((line, index) => { - ctx.fillText(line, x + w / 2, startY + index * lineHeight * 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(); -} - -/** - * 获取机器人状态 - * @param isWaring 是否告警 - * @param isFault 是否故障 - * @returns 机器人状态: 'fault' | 'warning' | 'normal' - * - * 判断逻辑: - * - isWaring=true, isFault=true → 故障 - * - isWaring=false, isFault=true → 故障 - * - isWaring=true, isFault=false → 告警 - * - isWaring=false, isFault=false → 正常 - */ -//#endregion - -//#region 辅助函数 -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]; -} +// 已拆分至 ./editor/editor-drawers.ts //#endregion diff --git a/src/services/editor/editor-drawers.ts b/src/services/editor/editor-drawers.ts new file mode 100644 index 0000000..151244b --- /dev/null +++ b/src/services/editor/editor-drawers.ts @@ -0,0 +1,412 @@ +import { + MapAreaType, + type MapPen, + MapPointType, + MapRoutePassType, + MapRouteType, + type Point, +} from '@api/map'; +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 || ''; + 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; + } + 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); + } + + 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]; +}