refactor: 移除 editor.service.ts 中的多个自定义绘制函数,优化代码结构并将其拆分至 editor-drawers.ts,提升可维护性

This commit is contained in:
xudan 2025-10-17 10:09:13 +08:00
parent fa5c88ad26
commit 20155894f8
2 changed files with 416 additions and 472 deletions

View File

@ -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

View File

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