web-map/src/services/editor/editor-drawers.ts

413 lines
16 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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