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]; }