refactor: 移除 editor.service.ts 中的多个自定义绘制函数,优化代码结构并将其拆分至 editor-drawers.ts,提升可维护性
This commit is contained in:
parent
fa5c88ad26
commit
20155894f8
@ -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
|
||||
|
412
src/services/editor/editor-drawers.ts
Normal file
412
src/services/editor/editor-drawers.ts
Normal 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];
|
||||
}
|
Loading…
x
Reference in New Issue
Block a user