refactor: 重构 EditorService,整合机器人相关逻辑至 EditorRobotService,简化代码结构并提升可维护性
This commit is contained in:
parent
de5c0e6bac
commit
ca69dbccf8
524
src/services/editor-robot.service.ts
Normal file
524
src/services/editor-robot.service.ts
Normal file
@ -0,0 +1,524 @@
|
||||
import type { MapPen, Rect } from '@api/map';
|
||||
import type { RobotGroup, RobotInfo, RobotLabel, RobotType } from '@api/robot';
|
||||
import type { SceneData } from '@api/scene';
|
||||
import sTheme from '@core/theme.service';
|
||||
import { CanvasLayer, LockState, type Meta2dStore, s8 } from '@meta2d/core';
|
||||
import { useObservable } from '@vueuse/rxjs';
|
||||
import { clone, get, isNil, nth, remove, some } from 'lodash-es';
|
||||
import { BehaviorSubject, debounceTime } from 'rxjs';
|
||||
import { reactive } from 'vue';
|
||||
|
||||
import cargoIcon from '../assets/icons/png/cargo.png';
|
||||
import chargingIcon from '../assets/icons/png/charging.png';
|
||||
import notAcceptingOrdersIcon from '../assets/icons/png/notAcceptingOrders.png';
|
||||
import colorConfig from './color/color-config.service';
|
||||
|
||||
interface RobotStatusPosition {
|
||||
x: number;
|
||||
y: number;
|
||||
rotate: number;
|
||||
}
|
||||
|
||||
export interface EditorRobotContext {
|
||||
store: Meta2dStore;
|
||||
data(): any;
|
||||
setValue(pen: Partial<MapPen> & { id: string }, options: { render: boolean; history: boolean; doEvent: boolean }): void;
|
||||
updatePen(id: string, pen: Partial<MapPen>, record?: boolean, render?: boolean): void;
|
||||
addPen(pen: MapPen, history?: boolean, storeData?: boolean, render?: boolean): Promise<MapPen>;
|
||||
top(pens: MapPen[]): void;
|
||||
render(): void;
|
||||
getPenById(id?: string): MapPen | undefined;
|
||||
getPenRect(pen: MapPen): Rect;
|
||||
find(target: string): MapPen[];
|
||||
inactive(): void;
|
||||
ensureCorrectLayerOrder(): void;
|
||||
}
|
||||
|
||||
export class EditorRobotService {
|
||||
private readonly robotMap = reactive<Map<RobotInfo['id'], RobotInfo>>(new Map());
|
||||
private readonly robotGroups$$ = new BehaviorSubject<RobotGroup[]>([]);
|
||||
private readonly robotLabels$$ = new BehaviorSubject<RobotLabel[]>([]);
|
||||
|
||||
public readonly robotGroups = useObservable<RobotGroup[]>(this.robotGroups$$.pipe(debounceTime(300)));
|
||||
public readonly robotLabels = useObservable<RobotLabel[]>(this.robotLabels$$.pipe(debounceTime(300)));
|
||||
|
||||
constructor(private readonly ctx: EditorRobotContext) {}
|
||||
|
||||
//#region 初始化与基础数据
|
||||
public loadInitialData(groups?: RobotGroup[], robots?: RobotInfo[], labels?: RobotLabel[]): void {
|
||||
this.robotMap.clear();
|
||||
robots?.forEach((v) => this.robotMap.set(v.id, v));
|
||||
this.robotGroups$$.next(groups ?? []);
|
||||
this.robotLabels$$.next(labels ?? []);
|
||||
this.syncRobots();
|
||||
this.syncRobotGroups();
|
||||
this.syncRobotLabels();
|
||||
}
|
||||
|
||||
public get robots(): RobotInfo[] {
|
||||
return Array.from(this.robotMap.values());
|
||||
}
|
||||
|
||||
public hasRobot(id: RobotInfo['id']): boolean {
|
||||
return this.robotMap.has(id);
|
||||
}
|
||||
|
||||
public getRobotById(id: RobotInfo['id']): RobotInfo | undefined {
|
||||
return this.robotMap.get(id);
|
||||
}
|
||||
|
||||
public updateRobot(id: RobotInfo['id'], value: Partial<RobotInfo>): void {
|
||||
const robot = this.getRobotById(id);
|
||||
if (isNil(robot)) return;
|
||||
this.robotMap.set(id, { ...robot, ...value });
|
||||
if (value.label) {
|
||||
this.ctx.setValue({ id, text: value.label }, { render: false, history: false, doEvent: false });
|
||||
}
|
||||
this.syncRobots();
|
||||
}
|
||||
|
||||
public addRobots(gid: RobotInfo['gid'], robots: RobotInfo[]): void {
|
||||
const groups = clone(this.robotGroups$$.value);
|
||||
const group = groups.find((v) => v.id === gid);
|
||||
if (isNil(group)) throw Error('未找到目标机器人组');
|
||||
group.robots ??= [];
|
||||
robots.forEach((robot) => {
|
||||
if (this.robotMap.has(robot.id)) return;
|
||||
this.robotMap.set(robot.id, { ...robot, gid });
|
||||
group.robots?.push(robot.id);
|
||||
});
|
||||
this.robotGroups$$.next(groups);
|
||||
this.syncRobots();
|
||||
this.syncRobotGroups();
|
||||
}
|
||||
|
||||
public removeRobots(ids: RobotInfo['id'][]): void {
|
||||
ids?.forEach((id) => this.robotMap.delete(id));
|
||||
const groups = clone(this.robotGroups$$.value);
|
||||
groups.forEach(({ robots }) => remove(robots ?? [], (id) => !this.robotMap.has(id)));
|
||||
this.robotGroups$$.next(groups);
|
||||
const labels = clone(this.robotLabels$$.value);
|
||||
labels.forEach(({ robots }) => remove(robots ?? [], (id) => !this.robotMap.has(id)));
|
||||
this.robotLabels$$.next(labels);
|
||||
this.syncRobots();
|
||||
this.syncRobotGroups();
|
||||
this.syncRobotLabels();
|
||||
}
|
||||
|
||||
public updateRobots(ids: RobotInfo['id'][], value: Partial<RobotInfo>): void {
|
||||
ids?.forEach((id) => {
|
||||
const robot = this.robotMap.get(id);
|
||||
if (isNil(robot)) return;
|
||||
this.robotMap.set(id, { ...robot, ...value });
|
||||
});
|
||||
this.syncRobots();
|
||||
}
|
||||
|
||||
public createRobotGroup(): void {
|
||||
const id = s8();
|
||||
const label = `RG${id}`;
|
||||
const groups = clone(this.robotGroups$$.value);
|
||||
groups.push({ id, label });
|
||||
this.robotGroups$$.next(groups);
|
||||
this.syncRobotGroups();
|
||||
}
|
||||
|
||||
public deleteRobotGroup(id: RobotGroup['id']): void {
|
||||
const groups = clone(this.robotGroups$$.value);
|
||||
const group = groups.find((v) => v.id === id);
|
||||
group?.robots?.forEach((rid) => this.robotMap.delete(rid));
|
||||
remove(groups, group);
|
||||
this.robotGroups$$.next(groups);
|
||||
this.syncRobots();
|
||||
this.syncRobotGroups();
|
||||
}
|
||||
|
||||
public updateRobotGroupLabel(id: RobotGroup['id'], label: RobotGroup['label']): void {
|
||||
const groups = this.robotGroups$$.value;
|
||||
const group = groups.find((v) => v.id === id);
|
||||
if (isNil(group)) throw Error('未找到目标机器人组');
|
||||
if (some(groups, ['label', label])) throw Error('机器人组名称已存在');
|
||||
group.label = label;
|
||||
this.robotGroups$$.next([...groups]);
|
||||
this.syncRobotGroups();
|
||||
}
|
||||
|
||||
public createRobotLabel(): void {
|
||||
const id = s8();
|
||||
const label = `RL${id}`;
|
||||
const labels = clone(this.robotLabels$$.value);
|
||||
labels.push({ id, label });
|
||||
this.robotLabels$$.next(labels);
|
||||
this.syncRobotLabels();
|
||||
}
|
||||
|
||||
public deleteRobotLabel(id: RobotLabel['id']): void {
|
||||
const labels = clone(this.robotLabels$$.value);
|
||||
const labelToDelete = labels.find((v) => v.id === id);
|
||||
if (!labelToDelete) return;
|
||||
|
||||
const robotsInLabel = labelToDelete.robots;
|
||||
if (robotsInLabel) {
|
||||
robotsInLabel.forEach((robotId) => {
|
||||
const robot = this.getRobotById(robotId);
|
||||
if (robot?.lid) {
|
||||
const newLid = robot.lid.filter((lid) => lid !== id);
|
||||
this.updateRobot(robot.id, { lid: newLid });
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
remove(labels, (l) => l.id === id);
|
||||
this.robotLabels$$.next(labels);
|
||||
this.syncRobotLabels();
|
||||
}
|
||||
|
||||
public updateRobotLabel(id: RobotLabel['id'], labelName: RobotLabel['label']): void {
|
||||
const labels = this.robotLabels$$.value;
|
||||
const label = labels.find((v) => v.id === id);
|
||||
if (isNil(label)) throw Error('未找到目标标签');
|
||||
if (some(labels, ['label', labelName])) throw Error('机器人标签名称已存在');
|
||||
label.label = labelName;
|
||||
this.robotLabels$$.next([...labels]);
|
||||
this.syncRobotLabels();
|
||||
}
|
||||
|
||||
public addRobotsToLabel(lid: RobotLabel['id'], robots: RobotInfo[]): void {
|
||||
const labels = clone(this.robotLabels$$.value);
|
||||
const label = labels.find((v) => v.id === lid);
|
||||
if (isNil(label)) throw Error('未找到目标标签');
|
||||
label.robots ??= [];
|
||||
|
||||
robots.forEach((robot) => {
|
||||
if (this.robotMap.has(robot.id)) {
|
||||
const existingRobot = this.robotMap.get(robot.id)!;
|
||||
existingRobot.lid ??= [];
|
||||
if (!existingRobot.lid.includes(lid)) {
|
||||
existingRobot.lid.push(lid);
|
||||
}
|
||||
if (!label.robots!.includes(robot.id)) {
|
||||
label.robots!.push(robot.id);
|
||||
}
|
||||
} else {
|
||||
const newRobot = { ...robot, lid: [lid] };
|
||||
this.robotMap.set(robot.id, newRobot);
|
||||
label.robots?.push(robot.id);
|
||||
}
|
||||
});
|
||||
|
||||
this.robotLabels$$.next(labels);
|
||||
this.syncRobots();
|
||||
this.syncRobotLabels();
|
||||
}
|
||||
|
||||
public removeRobotFromLabel(labelId: string, robotId: string): void {
|
||||
const labels = clone(this.robotLabels$$.value);
|
||||
const label = labels.find((v) => v.id === labelId);
|
||||
if (label?.robots) {
|
||||
remove(label.robots, (id) => id === robotId);
|
||||
}
|
||||
this.robotLabels$$.next(labels);
|
||||
|
||||
const robot = this.getRobotById(robotId);
|
||||
if (robot?.lid) {
|
||||
const newLid = robot.lid.filter((id) => id !== labelId);
|
||||
this.updateRobot(robotId, { lid: newLid });
|
||||
}
|
||||
|
||||
this.syncRobotLabels();
|
||||
this.syncRobots();
|
||||
}
|
||||
|
||||
public removeRobotsFromAllLabels(robotIds: string[]): void {
|
||||
robotIds.forEach((robotId) => {
|
||||
const robot = this.getRobotById(robotId);
|
||||
if (robot) {
|
||||
this.updateRobot(robotId, { lid: [] });
|
||||
}
|
||||
});
|
||||
|
||||
const labels = clone(this.robotLabels$$.value);
|
||||
labels.forEach((label) => {
|
||||
if (label.robots) {
|
||||
remove(label.robots, (id) => robotIds.includes(id));
|
||||
}
|
||||
});
|
||||
this.robotLabels$$.next(labels);
|
||||
this.syncRobots();
|
||||
this.syncRobotLabels();
|
||||
}
|
||||
//#endregion
|
||||
|
||||
//#region 画布绘制
|
||||
public async initRobots(): Promise<void> {
|
||||
await Promise.all(
|
||||
this.robots.map(async ({ id, label, type }) => {
|
||||
const pen: MapPen = {
|
||||
...this.mapRobotImage(type, true, label),
|
||||
id,
|
||||
name: 'robot',
|
||||
tags: ['robot'],
|
||||
x: 0,
|
||||
y: 0,
|
||||
width: 120,
|
||||
height: 120,
|
||||
lineWidth: 1,
|
||||
robot: { type },
|
||||
visible: false,
|
||||
text: label,
|
||||
textTop: -18,
|
||||
whiteSpace: 'nowrap',
|
||||
ellipsis: false,
|
||||
locked: LockState.Disable,
|
||||
};
|
||||
await this.ctx.addPen(pen, false, true, true);
|
||||
this.updateRobotStatusOverlay(id, false);
|
||||
}),
|
||||
);
|
||||
|
||||
this.ctx.ensureCorrectLayerOrder();
|
||||
}
|
||||
|
||||
public updateAllRobotImageSizes(): void {
|
||||
this.robots.forEach(({ id, type, label }) => {
|
||||
const pen = this.ctx.getPenById(id);
|
||||
if (pen && pen.robot) {
|
||||
const imageConfig = this.mapRobotImage(type, pen.robot.active, label);
|
||||
this.ctx.setValue(
|
||||
{
|
||||
id,
|
||||
iconWidth: imageConfig.iconWidth,
|
||||
iconHeight: imageConfig.iconHeight,
|
||||
iconTop: imageConfig.iconTop,
|
||||
},
|
||||
{ render: true, history: false, doEvent: false },
|
||||
);
|
||||
this.updateRobotStatusOverlay(id, true);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public updateRobotImage(robotName: string): void {
|
||||
const robot = this.robots.find((r) => r.label === robotName);
|
||||
if (!robot) return;
|
||||
|
||||
const pen = this.ctx.getPenById(robot.id);
|
||||
if (pen && pen.robot) {
|
||||
const imageConfig = this.mapRobotImage(robot.type, pen.robot.active, robotName);
|
||||
this.ctx.setValue(
|
||||
{
|
||||
id: robot.id,
|
||||
image: imageConfig.image,
|
||||
iconWidth: imageConfig.iconWidth,
|
||||
iconHeight: imageConfig.iconHeight,
|
||||
iconTop: imageConfig.iconTop,
|
||||
},
|
||||
{ render: true, history: false, doEvent: false },
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
public updateRobotStatusOverlay(id: string, render = false, newPosition?: RobotStatusPosition): void {
|
||||
const pen = this.ctx.getPenById(id);
|
||||
if (!pen) return;
|
||||
|
||||
const icon = this.getRobotStatusIcon(pen);
|
||||
const robotVisible = pen.visible !== false;
|
||||
|
||||
const baseW = (pen as any).iconWidth ?? 42;
|
||||
const baseH = (pen as any).iconHeight ?? 76;
|
||||
const oW = Math.max(8, Math.floor(baseW * 0.5));
|
||||
const oH = Math.max(8, Math.floor(baseH * 0.5));
|
||||
|
||||
const rect = this.ctx.getPenRect(pen);
|
||||
const deg: number = newPosition?.rotate ?? pen.rotate ?? 0;
|
||||
const theta = (deg * Math.PI) / 180;
|
||||
const cx = (newPosition?.x ?? rect.x) + rect.width / 2;
|
||||
const cy = (newPosition?.y ?? rect.y) + rect.height / 2;
|
||||
const iconTop = (pen as any).iconTop ?? 0;
|
||||
const localDx = 0;
|
||||
const localDy = iconTop + 16;
|
||||
const rotDx = localDx * Math.cos(theta) - localDy * Math.sin(theta);
|
||||
const rotDy = localDx * Math.sin(theta) + localDy * Math.cos(theta);
|
||||
const icx = cx + rotDx;
|
||||
const icy = cy + rotDy;
|
||||
const ox = icx - oW / 2;
|
||||
const oy = icy - oH / 2;
|
||||
|
||||
const overlayId = `robot-status-${id}`;
|
||||
const exist = this.ctx.getPenById(overlayId);
|
||||
|
||||
if (!robotVisible) {
|
||||
if (exist) {
|
||||
this.ctx.setValue({ id: overlayId, visible: false }, { render, history: false, doEvent: false });
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if (!icon) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (exist) {
|
||||
this.ctx.setValue(
|
||||
{
|
||||
id: overlayId,
|
||||
image: icon,
|
||||
x: ox,
|
||||
y: oy,
|
||||
width: oW,
|
||||
height: oH,
|
||||
rotate: deg,
|
||||
visible: true,
|
||||
locked: LockState.Disable,
|
||||
},
|
||||
{ render: false, history: false, doEvent: false },
|
||||
);
|
||||
this.ctx.top([exist]);
|
||||
if (render) this.ctx.render();
|
||||
return;
|
||||
}
|
||||
|
||||
const overlayPen: MapPen = {
|
||||
id: overlayId,
|
||||
name: 'image',
|
||||
tags: ['robot-status'],
|
||||
x: ox,
|
||||
y: oy,
|
||||
width: oW,
|
||||
height: oH,
|
||||
image: icon,
|
||||
rotate: deg,
|
||||
canvasLayer: CanvasLayer.CanvasImage,
|
||||
locked: LockState.Disable,
|
||||
visible: true,
|
||||
} as any;
|
||||
this.ctx.addPen(overlayPen, false, false, true);
|
||||
this.ctx.top([overlayPen]);
|
||||
if (render) this.ctx.render();
|
||||
}
|
||||
|
||||
public mapRobotImage(type: RobotType, active?: boolean, robotName?: string): Required<Pick<MapPen, 'image' | 'iconWidth' | 'iconHeight' | 'iconTop' | 'canvasLayer'>> {
|
||||
const theme = this.ctx.data().theme;
|
||||
const useCustomImages = colorConfig.getColor('robot.useCustomImages') === 'true';
|
||||
|
||||
let image: string;
|
||||
if (useCustomImages && robotName) {
|
||||
const customImage = colorConfig.getRobotCustomImage(robotName, active ? 'active' : 'normal');
|
||||
if (customImage) {
|
||||
image = customImage;
|
||||
} else {
|
||||
image = import.meta.env.BASE_URL + (active ? `/robot/${type}-active-${theme}.png` : `/robot/${type}-${theme}.png`);
|
||||
}
|
||||
} else {
|
||||
image = import.meta.env.BASE_URL + (active ? `/robot/${type}-active-${theme}.png` : `/robot/${type}-${theme}.png`);
|
||||
}
|
||||
|
||||
const iconWidth = colorConfig.getColor('robot.imageWidth') ? Number(colorConfig.getColor('robot.imageWidth')) : 42;
|
||||
const iconHeight = colorConfig.getColor('robot.imageHeight') ? Number(colorConfig.getColor('robot.imageHeight')) : 76;
|
||||
const iconTop = this.calculatePixelAlignedOffset(-16);
|
||||
|
||||
return { image, iconWidth, iconHeight, iconTop, canvasLayer: CanvasLayer.CanvasImage };
|
||||
}
|
||||
//#endregion
|
||||
|
||||
private getRobotStatusIcon(pen?: MapPen): string | null {
|
||||
if (!pen) return null;
|
||||
const r1: any = pen.robot ?? {};
|
||||
const r2 = this.getRobotById(pen.id || '');
|
||||
const toBool = (v: any) => v === true || v === 1 || v === '1';
|
||||
const isCarrying = toBool(r1?.isCarrying ?? r2?.isCarrying);
|
||||
const canOrder = toBool(r1?.canOrder ?? r2?.canOrder);
|
||||
const isCharging = toBool(r1?.isCharging ?? r2?.isCharging);
|
||||
|
||||
if (isCarrying) return cargoIcon;
|
||||
if (!canOrder) return notAcceptingOrdersIcon;
|
||||
if (isCharging) return chargingIcon;
|
||||
return null;
|
||||
}
|
||||
|
||||
private calculatePixelAlignedOffset(baseOffset: number): number {
|
||||
const ratio = window.devicePixelRatio || 1;
|
||||
const scaled = baseOffset * ratio;
|
||||
const snapped = Math.round(scaled);
|
||||
const logicalOffset = snapped / ratio;
|
||||
return this.fixPrecision(logicalOffset);
|
||||
}
|
||||
|
||||
private fixPrecision(value: number): number {
|
||||
const truncated = Math.floor(value * 1000) / 1000;
|
||||
return parseFloat(truncated.toFixed(3));
|
||||
}
|
||||
|
||||
private syncRobots(): void {
|
||||
(this.ctx.store.data as SceneData).robots = [...this.robotMap.values()];
|
||||
}
|
||||
|
||||
private syncRobotGroups(): void {
|
||||
(this.ctx.store.data as SceneData).robotGroups = this.robotGroups$$.value;
|
||||
}
|
||||
|
||||
private syncRobotLabels(): void {
|
||||
(this.ctx.store.data as SceneData).robotLabels = this.robotLabels$$.value;
|
||||
}
|
||||
}
|
||||
|
||||
function getRobotStatus(isWaring?: boolean, isFault?: boolean): 'fault' | 'warning' | 'normal' {
|
||||
if (isFault) return 'fault';
|
||||
if (isWaring) return 'warning';
|
||||
return 'normal';
|
||||
}
|
||||
|
||||
export function drawRobot(ctx: CanvasRenderingContext2D, pen: MapPen): void {
|
||||
const theme = sTheme.editor;
|
||||
const { lineWidth: s = 1 } = pen.calculative ?? {};
|
||||
const { x = 0, y = 0, width: w = 0, height: h = 0, rotate: deg = 0 } = pen.calculative?.worldRect ?? {};
|
||||
const { active, path, isWaring, isFault } = pen.robot ?? {};
|
||||
|
||||
if (!active) return;
|
||||
|
||||
const status = getRobotStatus(isWaring, isFault);
|
||||
|
||||
const imageWidth = colorConfig.getColor('robot.imageWidth') ? Number(colorConfig.getColor('robot.imageWidth')) : 60;
|
||||
const imageHeight = colorConfig.getColor('robot.imageHeight') ? Number(colorConfig.getColor('robot.imageHeight')) : 80;
|
||||
const imageSize = Math.max(imageWidth, imageHeight);
|
||||
const robotRadius = (imageSize * 1.2 * s) / 2;
|
||||
|
||||
const ox = x + w / 2;
|
||||
const oy = y + h / 2;
|
||||
ctx.save();
|
||||
ctx.beginPath();
|
||||
ctx.arc(ox, oy, robotRadius, 0, Math.PI * 2);
|
||||
ctx.fillStyle =
|
||||
colorConfig.getColor(`robot.fill${status === 'normal' ? 'Normal' : status === 'warning' ? 'Warning' : 'Fault'}`) ||
|
||||
(get(theme, `robot.fill-${status}`) ?? get(theme, 'robot.fill') ?? '');
|
||||
ctx.fill();
|
||||
ctx.strokeStyle =
|
||||
colorConfig.getColor(`robot.stroke${status === 'normal' ? 'Normal' : status === 'warning' ? 'Warning' : 'Fault'}`) ||
|
||||
(get(theme, `robot.stroke-${status}`) ?? get(theme, 'robot.stroke') ?? '');
|
||||
ctx.stroke();
|
||||
if (path?.length) {
|
||||
ctx.strokeStyle = colorConfig.getColor('robot.line') || (get(theme, 'robot.line') ?? '');
|
||||
ctx.lineCap = 'round';
|
||||
ctx.lineWidth = s * 4;
|
||||
ctx.setLineDash([s * 5, s * 10]);
|
||||
ctx.translate(ox, oy);
|
||||
ctx.rotate((-deg * Math.PI) / 180);
|
||||
ctx.beginPath();
|
||||
ctx.moveTo(0, 0);
|
||||
path.forEach((d) => ctx.lineTo(d.x * s, d.y * s));
|
||||
ctx.stroke();
|
||||
const { x: ex1 = 0, y: ey1 = 0 } = nth(path, -1) ?? {};
|
||||
const { x: ex2 = 0, y: ey2 = 0 } = nth(path, -2) ?? {};
|
||||
const r = Math.atan2(ey1 - ey2, ex1 - ex2) + Math.PI;
|
||||
ctx.setLineDash([0]);
|
||||
ctx.translate(ex1 * s, ey1 * s);
|
||||
ctx.beginPath();
|
||||
ctx.moveTo(Math.cos(r + Math.PI / 4) * s * 10, Math.sin(r + Math.PI / 4) * s * 10);
|
||||
ctx.lineTo(0, 0);
|
||||
ctx.lineTo(Math.cos(r - Math.PI / 4) * s * 10, Math.sin(r - Math.PI / 4) * s * 10);
|
||||
ctx.stroke();
|
||||
ctx.setTransform(1, 0, 0, 1, 0, 0);
|
||||
}
|
||||
ctx.restore();
|
||||
}
|
@ -11,7 +11,7 @@ import {
|
||||
type Point,
|
||||
type Rect,
|
||||
} from '@api/map';
|
||||
import type { RobotGroup, RobotInfo, RobotLabel,RobotType } from '@api/robot';
|
||||
import type { RobotGroup, RobotInfo, RobotLabel } from '@api/robot';
|
||||
import type {
|
||||
GroupSceneDetail,
|
||||
SceneData,
|
||||
@ -23,14 +23,11 @@ import type {
|
||||
import sTheme from '@core/theme.service';
|
||||
import { CanvasLayer, LockState, Meta2d, type Meta2dStore, type Pen, s8 } from '@meta2d/core';
|
||||
import { useObservable } from '@vueuse/rxjs';
|
||||
import { clone, get, isEmpty, isNil, isString, nth, pick, remove, some } from 'lodash-es';
|
||||
import { BehaviorSubject, debounceTime, filter, map, Subject, switchMap } from 'rxjs';
|
||||
import { reactive, watch } from 'vue';
|
||||
import { clone, get, isEmpty, isNil, isString, pick, } from 'lodash-es';
|
||||
import { debounceTime, filter, map, Subject, switchMap } from 'rxjs';
|
||||
import type { Ref } from 'vue';
|
||||
import { watch } from 'vue';
|
||||
|
||||
// 机器人状态覆盖图标
|
||||
import cargoIcon from '../assets/icons/png/cargo.png';
|
||||
import chargingIcon from '../assets/icons/png/charging.png';
|
||||
import notAcceptingOrdersIcon from '../assets/icons/png/notAcceptingOrders.png';
|
||||
import { AreaOperationService } from './area-operation.service';
|
||||
import { BinTaskManagerService } from './bintask-manager.service';
|
||||
import colorConfig from './color/color-config.service';
|
||||
@ -39,6 +36,7 @@ import {
|
||||
drawStorageLocation,
|
||||
drawStorageMore,
|
||||
} from './draw/storage-location-drawer';
|
||||
import { drawRobot,EditorRobotService } from './editor-robot.service';
|
||||
import { LayerManagerService } from './layer-manager.service';
|
||||
import { createStorageLocationUpdater,StorageLocationService } from './storage-location.service';
|
||||
import { AutoStorageGenerator } from './utils/auto-storage-generator';
|
||||
@ -59,6 +57,7 @@ export class EditorService extends Meta2d {
|
||||
private readonly layerManager: LayerManagerService;
|
||||
/** 库位服务实例 */
|
||||
private readonly storageLocationService: StorageLocationService;
|
||||
private readonly robotService: EditorRobotService;
|
||||
|
||||
/** 区域操作服务实例 */
|
||||
private readonly areaOperationService!: AreaOperationService;
|
||||
@ -94,14 +93,13 @@ export class EditorService extends Meta2d {
|
||||
|
||||
this.open();
|
||||
this.setState(editable);
|
||||
this.#loadRobots(robotGroups, robots);
|
||||
this.#loadLabels(robotLabels);
|
||||
this.robotService.loadInitialData(robotGroups, robots, robotLabels);
|
||||
await this.#loadScenePoints(points, isImport);
|
||||
this.#loadSceneRoutes(routes, isImport);
|
||||
await this.#loadSceneAreas(areas, isImport);
|
||||
|
||||
// 确保正确的层级顺序:路线 < 点位 < 机器人
|
||||
this.#ensureCorrectLayerOrder();
|
||||
this.ensureCorrectLayerOrder();
|
||||
|
||||
// 为所有动作点创建库位pen对象
|
||||
this.createAllStorageLocationPens();
|
||||
@ -201,14 +199,8 @@ export class EditorService extends Meta2d {
|
||||
* @param groups 机器人组列表
|
||||
* @param robots 机器人信息列表
|
||||
*/
|
||||
#loadRobots(groups?: RobotGroup[], robots?: RobotInfo[]): void {
|
||||
this.#robotMap.clear();
|
||||
robots?.forEach((v) => this.#robotMap.set(v.id, v));
|
||||
this.#robotGroups$$.next(groups ?? []);
|
||||
}
|
||||
#loadLabels(labels?: RobotLabel[]): void {
|
||||
this.#robotLabels$$.next(labels ?? []);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 从场景数据加载点位到画布
|
||||
* @param points 标准场景点位数据数组
|
||||
@ -410,7 +402,7 @@ export class EditorService extends Meta2d {
|
||||
properties,
|
||||
};
|
||||
if ([MapPointType.充电点, MapPointType.停靠点].includes(type)) {
|
||||
point.robots = robots?.filter((v) => this.#robotMap.has(v));
|
||||
point.robots = robots?.filter((v) => this.robotService.hasRobot(v));
|
||||
// 若未提供 enabled,则默认启用
|
||||
point.enabled = (enabled ?? 1) as 0 | 1;
|
||||
}
|
||||
@ -526,7 +518,7 @@ export class EditorService extends Meta2d {
|
||||
* 确保正确的层级顺序
|
||||
* 委托给图层管理服务处理
|
||||
*/
|
||||
#ensureCorrectLayerOrder(): void {
|
||||
public ensureCorrectLayerOrder(): void {
|
||||
this.layerManager.ensureCorrectLayerOrder();
|
||||
}
|
||||
//#endregion
|
||||
@ -579,184 +571,94 @@ export class EditorService extends Meta2d {
|
||||
),
|
||||
);
|
||||
|
||||
//#region 机器人组管理
|
||||
/** 机器人信息映射表,响应式存储所有机器人数据 */
|
||||
readonly #robotMap = reactive<Map<RobotInfo['id'], RobotInfo>>(new Map());
|
||||
|
||||
/** 获取所有机器人信息数组 */
|
||||
//#region 机器人服务
|
||||
public get robots(): RobotInfo[] {
|
||||
return Array.from(this.#robotMap.values());
|
||||
return this.robotService.robots;
|
||||
}
|
||||
|
||||
public get robotGroups(): Ref<RobotGroup[] | undefined> {
|
||||
return this.robotService.robotGroups;
|
||||
}
|
||||
|
||||
public get robotLabels(): Ref<RobotLabel[] | undefined> {
|
||||
return this.robotService.robotLabels;
|
||||
}
|
||||
|
||||
public checkRobotById(id: RobotInfo['id']): boolean {
|
||||
return this.#robotMap.has(id);
|
||||
return this.robotService.hasRobot(id);
|
||||
}
|
||||
|
||||
public getRobotById(id: RobotInfo['id']): RobotInfo | undefined {
|
||||
return this.#robotMap.get(id);
|
||||
return this.robotService.getRobotById(id);
|
||||
}
|
||||
|
||||
public updateRobot(id: RobotInfo['id'], value: Partial<RobotInfo>): void {
|
||||
const robot = this.getRobotById(id);
|
||||
if (isNil(robot)) return;
|
||||
this.#robotMap.set(id, { ...robot, ...value });
|
||||
if (value.label) {
|
||||
this.setValue({ id, text: value.label }, { render: false, history: false, doEvent: false });
|
||||
}
|
||||
(<SceneData>this.store.data).robots = [...this.#robotMap.values()];
|
||||
this.robotService.updateRobot(id, value);
|
||||
}
|
||||
|
||||
public addRobots(gid: RobotInfo['gid'], robots: RobotInfo[]): void {
|
||||
const groups = clone(this.#robotGroups$$.value);
|
||||
const group = groups.find((v) => v.id === gid);
|
||||
if (isNil(group)) throw Error('未找到目标机器人组');
|
||||
group.robots ??= [];
|
||||
robots.forEach((v) => {
|
||||
if (this.#robotMap.has(v.id)) return;
|
||||
this.#robotMap.set(v.id, { ...v, gid });
|
||||
group.robots?.push(v.id);
|
||||
});
|
||||
this.#robotGroups$$.next(groups);
|
||||
(<SceneData>this.store.data).robots = [...this.#robotMap.values()];
|
||||
(<SceneData>this.store.data).robotGroups = this.#robotGroups$$.value;
|
||||
this.robotService.addRobots(gid, robots);
|
||||
}
|
||||
|
||||
public removeRobots(ids: RobotInfo['id'][]): void {
|
||||
ids?.forEach((v) => this.#robotMap.delete(v));
|
||||
const groups = clone(this.#robotGroups$$.value);
|
||||
groups.forEach(({ robots }) => remove(robots ?? [], (v) => !this.#robotMap.has(v)));
|
||||
this.#robotGroups$$.next(groups);
|
||||
const labels = clone(this.#robotLabels$$.value);
|
||||
labels.forEach(({ robots }) => remove(robots ?? [], (v) => !this.#robotMap.has(v)));
|
||||
this.#robotLabels$$.next(labels);
|
||||
(<SceneData>this.store.data).robots = [...this.#robotMap.values()];
|
||||
(<SceneData>this.store.data).robotGroups = this.#robotGroups$$.value;
|
||||
(<SceneData>this.store.data).robotLabels = this.#robotLabels$$.value;
|
||||
this.robotService.removeRobots(ids);
|
||||
}
|
||||
|
||||
public updateRobots(ids: RobotInfo['id'][], value: Partial<RobotInfo>): void {
|
||||
ids?.forEach((v) => {
|
||||
const robot = this.#robotMap.get(v);
|
||||
if (isNil(robot)) return;
|
||||
this.#robotMap.set(v, { ...robot, ...value });
|
||||
});
|
||||
(<SceneData>this.store.data).robots = [...this.#robotMap.values()];
|
||||
this.robotService.updateRobots(ids, value);
|
||||
}
|
||||
|
||||
readonly #robotGroups$$ = new BehaviorSubject<RobotGroup[]>([]);
|
||||
public readonly robotGroups = useObservable<RobotGroup[]>(this.#robotGroups$$.pipe(debounceTime(300)));
|
||||
|
||||
readonly #robotLabels$$ = new BehaviorSubject<RobotLabel[]>([]);
|
||||
public readonly robotLabels = useObservable<RobotLabel[]>(this.#robotLabels$$.pipe(debounceTime(300)));
|
||||
|
||||
public createRobotGroup(): void {
|
||||
const id = s8();
|
||||
const label = `RG${id}`;
|
||||
const groups = clone(this.#robotGroups$$.value);
|
||||
groups.push({ id, label });
|
||||
this.#robotGroups$$.next(groups);
|
||||
(<SceneData>this.store.data).robotGroups = this.#robotGroups$$.value;
|
||||
this.robotService.createRobotGroup();
|
||||
}
|
||||
|
||||
public deleteRobotGroup(id: RobotGroup['id']): void {
|
||||
const groups = clone(this.#robotGroups$$.value);
|
||||
const group = groups.find((v) => v.id === id);
|
||||
group?.robots?.forEach((v) => this.#robotMap.delete(v));
|
||||
remove(groups, group);
|
||||
this.#robotGroups$$.next(groups);
|
||||
(<SceneData>this.store.data).robots = [...this.#robotMap.values()];
|
||||
(<SceneData>this.store.data).robotGroups = this.#robotGroups$$.value;
|
||||
this.robotService.deleteRobotGroup(id);
|
||||
}
|
||||
|
||||
public updateRobotGroupLabel(id: RobotGroup['id'], label: RobotGroup['label']): void {
|
||||
const groups = this.#robotGroups$$.value;
|
||||
const group = groups.find((v) => v.id === id);
|
||||
if (isNil(group)) throw Error('未找到目标机器人组');
|
||||
if (some(groups, ['label', label])) throw Error('机器人组名称已经存在');
|
||||
group.label = label;
|
||||
this.#robotGroups$$.next([...groups]);
|
||||
(<SceneData>this.store.data).robotGroups = this.#robotGroups$$.value;
|
||||
this.robotService.updateRobotGroupLabel(id, label);
|
||||
}
|
||||
|
||||
public createRobotLabel(): void {
|
||||
const id = s8();
|
||||
const label = `RL${id}`;
|
||||
const labels = clone(this.#robotLabels$$.value);
|
||||
labels.push({ id, label });
|
||||
this.#robotLabels$$.next(labels);
|
||||
(<SceneData>this.store.data).robotLabels = this.#robotLabels$$.value;
|
||||
this.robotService.createRobotLabel();
|
||||
}
|
||||
|
||||
public deleteRobotLabel(id: RobotLabel['id']): void {
|
||||
const labels = clone(this.#robotLabels$$.value);
|
||||
const labelToDelete = labels.find((v) => v.id === id);
|
||||
if (!labelToDelete) return;
|
||||
|
||||
// Remove the label id from all robots
|
||||
const robotsInLabel = labelToDelete.robots;
|
||||
if (robotsInLabel) {
|
||||
robotsInLabel.forEach((robotId) => {
|
||||
const robot = this.getRobotById(robotId);
|
||||
if (robot?.lid) {
|
||||
const newLid = robot.lid.filter((lid) => lid !== id);
|
||||
this.updateRobot(robot.id, { lid: newLid });
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
remove(labels, (l) => l.id === id);
|
||||
this.#robotLabels$$.next(labels);
|
||||
(<SceneData>this.store.data).robotLabels = this.#robotLabels$$.value;
|
||||
this.robotService.deleteRobotLabel(id);
|
||||
}
|
||||
|
||||
public updateRobotLabel(id: RobotLabel['id'], labelName: RobotLabel['label']): void {
|
||||
const labels = this.#robotLabels$$.value;
|
||||
const label = labels.find((v) => v.id === id);
|
||||
if (isNil(label)) throw Error('未找到目标机器人标签');
|
||||
if (some(labels, (l) => l.label === labelName && l.id !== id)) throw Error('机器人标签名称已经存在');
|
||||
label.label = labelName;
|
||||
this.#robotLabels$$.next([...labels]);
|
||||
(<SceneData>this.store.data).robotLabels = this.#robotLabels$$.value;
|
||||
this.robotService.updateRobotLabel(id, labelName);
|
||||
}
|
||||
|
||||
public addRobotsToLabel(lid: RobotLabel['id'], robots: RobotInfo[]): void {
|
||||
const labels = clone(this.#robotLabels$$.value);
|
||||
const label = labels.find((v) => v.id === lid);
|
||||
if (isNil(label)) throw Error('未找到目标机器人标签');
|
||||
label.robots ??= [];
|
||||
robots.forEach((v) => {
|
||||
const existingRobot = this.#robotMap.get(v.id);
|
||||
if (existingRobot) {
|
||||
// If robot is already in scene
|
||||
if (!existingRobot.lid) {
|
||||
existingRobot.lid = [];
|
||||
}
|
||||
if (!existingRobot.lid.includes(lid)) {
|
||||
existingRobot.lid.push(lid);
|
||||
}
|
||||
if (label.robots && !label.robots.includes(v.id)) {
|
||||
label.robots.push(v.id);
|
||||
}
|
||||
} else {
|
||||
// If robot is new to the scene
|
||||
const newRobot = { ...v, lid: [lid] };
|
||||
this.#robotMap.set(v.id, newRobot);
|
||||
if (label.robots) {
|
||||
label.robots.push(v.id);
|
||||
}
|
||||
}
|
||||
});
|
||||
this.#robotLabels$$.next(labels);
|
||||
(<SceneData>this.store.data).robots = [...this.#robotMap.values()];
|
||||
(<SceneData>this.store.data).robotLabels = this.#robotLabels$$.value;
|
||||
this.robotService.addRobotsToLabel(lid, robots);
|
||||
}
|
||||
|
||||
public removeRobotFromLabel(labelId: string, robotId: string): void {
|
||||
const labels = clone(this.#robotLabels$$.value);
|
||||
const label = labels.find((v) => v.id === labelId);
|
||||
if (label?.robots) {
|
||||
remove(label.robots, (id) => id === robotId);
|
||||
}
|
||||
this.#robotLabels$$.next(labels);
|
||||
this.robotService.removeRobotFromLabel(labelId, robotId);
|
||||
}
|
||||
|
||||
const robot = this.getRobotById(robotId);
|
||||
if (robot?.lid) {
|
||||
const newLid = robot.lid.filter((id) => id !== labelId);
|
||||
this.updateRobot(robotId, { lid: newLid });
|
||||
}
|
||||
public removeRobotsFromAllLabels(robotIds: string[]): void {
|
||||
this.robotService.removeRobotsFromAllLabels(robotIds);
|
||||
}
|
||||
|
||||
(<SceneData>this.store.data).robotLabels = this.#robotLabels$$.value;
|
||||
(<SceneData>this.store.data).robots = [...this.#robotMap.values()];
|
||||
|
||||
public async initRobots(): Promise<void> {
|
||||
await this.robotService.initRobots();
|
||||
}
|
||||
|
||||
public updateAllRobotImageSizes(): void {
|
||||
this.robotService.updateAllRobotImageSizes();
|
||||
}
|
||||
|
||||
public updateRobotImage(robotName: string): void {
|
||||
this.robotService.updateRobotImage(robotName);
|
||||
}
|
||||
|
||||
public updateRobotStatusOverlay(id: string, render = false, newPosition?: { x: number; y: number; rotate: number }): void {
|
||||
this.robotService.updateRobotStatusOverlay(id, render, newPosition);
|
||||
}
|
||||
//#endregion
|
||||
|
||||
@ -837,44 +739,7 @@ export class EditorService extends Meta2d {
|
||||
* 优化的像素对齐算法 - 确保在所有缩放比例下都能精确对齐像素边界
|
||||
* 解决小车和光圈在特定缩放比例下不重合的问题
|
||||
*/
|
||||
#calculatePixelAlignedOffset(baseOffset: number): number {
|
||||
const scale = this.store.data.scale || 1;
|
||||
const devicePixelRatio = window.devicePixelRatio || 1;
|
||||
|
||||
// 计算实际像素偏移量
|
||||
const scaledOffset = baseOffset * scale;
|
||||
|
||||
// 多重对齐策略:
|
||||
// 1. 设备像素对齐 - 确保在高DPI屏幕上也能对齐
|
||||
const deviceAlignedOffset = Math.round(scaledOffset * devicePixelRatio) / devicePixelRatio;
|
||||
|
||||
// 2. 子像素对齐 - 对于常见的缩放比例使用特殊处理
|
||||
let finalOffset = deviceAlignedOffset;
|
||||
|
||||
// 针对常见问题缩放比例的特殊处理
|
||||
const roundedScale = Math.round(scale * 100) / 100; // 避免浮点数精度问题
|
||||
|
||||
if (roundedScale <= 0.2) {
|
||||
// 极小缩放:使用更粗粒度的对齐(0.5像素边界)
|
||||
finalOffset = Math.round(scaledOffset * 2) / 2;
|
||||
} else if (roundedScale <= 0.5) {
|
||||
// 小缩放:使用0.25像素边界对齐
|
||||
finalOffset = Math.round(scaledOffset * 4) / 4;
|
||||
} else if (roundedScale >= 2) {
|
||||
// 大缩放:使用精确的像素边界对齐
|
||||
finalOffset = Math.round(scaledOffset);
|
||||
} else {
|
||||
// 标准缩放:使用设备像素对齐结果,但增加额外的精度控制
|
||||
const precisionFactor = 8; // 1/8像素精度
|
||||
finalOffset = Math.round(scaledOffset * precisionFactor) / precisionFactor;
|
||||
}
|
||||
|
||||
// 3. 转换回逻辑坐标系并应用精度控制
|
||||
const logicalOffset = finalOffset / scale;
|
||||
|
||||
// 4. 使用现有的精度控制方法确保数值稳定性
|
||||
return this.#fixPrecision(logicalOffset);
|
||||
}
|
||||
|
||||
|
||||
/** 画布变化事件流,用于触发响应式数据更新 */
|
||||
readonly #change$$ = new Subject<boolean>();
|
||||
@ -1022,7 +887,6 @@ export class EditorService extends Meta2d {
|
||||
return createStorageLocationUpdater(this.storageLocationService);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 为所有动作点创建库位pen对象
|
||||
* 用于初始化或刷新所有动作点的库位显示
|
||||
@ -1080,8 +944,6 @@ export class EditorService extends Meta2d {
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* 根据设备ID更新自动门点状态
|
||||
* @param deviceId 设备ID
|
||||
@ -1174,281 +1036,6 @@ export class EditorService extends Meta2d {
|
||||
}
|
||||
}
|
||||
|
||||
//#region 实时机器人
|
||||
public async initRobots(): Promise<void> {
|
||||
await Promise.all(
|
||||
this.robots.map(async ({ id, label, type }) => {
|
||||
const pen: MapPen = {
|
||||
...this.#mapRobotImage(type, true, label), // 传递机器人名称(label)
|
||||
id,
|
||||
name: 'robot',
|
||||
tags: ['robot'],
|
||||
x: 0,
|
||||
y: 0,
|
||||
width: 120,
|
||||
height: 120,
|
||||
lineWidth: 1,
|
||||
robot: { type },
|
||||
visible: false,
|
||||
text: label,
|
||||
textTop: -18, // 调整标签位置,更靠近机器人本体,减少被覆盖的可能性
|
||||
whiteSpace: 'nowrap',
|
||||
ellipsis: false,
|
||||
locked: LockState.Disable,
|
||||
};
|
||||
await this.addPen(pen, false, true, true);
|
||||
// 初始化时创建/同步一次状态覆盖图标(默认隐藏或根据当前状态显示)
|
||||
this.updateRobotStatusOverlay(id, false);
|
||||
}),
|
||||
);
|
||||
|
||||
// 机器人初始化完成后,确保层级正确(机器人应在最顶层)
|
||||
this.#ensureCorrectLayerOrder();
|
||||
|
||||
}
|
||||
|
||||
// public refreshRobot(id: RobotInfo['id'], info: Partial<RobotRealtimeInfo>): void {
|
||||
// const pen = this.getPenById(id);
|
||||
// const { rotate: or, robot } = pen ?? {};
|
||||
// if (!robot?.type) return;
|
||||
// const { x: ox, y: oy } = this.getPenRect(pen!);
|
||||
// const { x: cx = 37, y: cy = 37, active, angle, path: points, isWaring, isFault } = info;
|
||||
// const x = cx - 60;
|
||||
// const y = cy - 60;
|
||||
// const rotate = angle ?? or;
|
||||
// const path =
|
||||
// points?.map((p) => ({ x: p.x - cx, y: p.y - cy })) ??
|
||||
// robot.path?.map((p) => ({ x: p.x + ox! - x, y: p.y + oy! - y }));
|
||||
// const o = { ...robot, ...omitBy({ active, path, isWaring, isFault }, isNil) };
|
||||
// if (isNil(active)) {
|
||||
// this.setValue(
|
||||
// { id, x, y, rotate, robot: o, visible: true, locked: LockState.None },
|
||||
// { render: true, history: false, doEvent: false }
|
||||
// );
|
||||
// } else {
|
||||
// this.setValue(
|
||||
// {
|
||||
// id,
|
||||
// ...this.#mapRobotImage(robot.type, active),
|
||||
// x, y, rotate, robot: o,
|
||||
// visible: true,
|
||||
// locked: LockState.None
|
||||
// },
|
||||
// { render: true, history: false, doEvent: false },
|
||||
// );
|
||||
// }
|
||||
|
||||
|
||||
|
||||
// }
|
||||
|
||||
/**
|
||||
* 更新所有机器人的图片尺寸
|
||||
* 当机器人图片尺寸配置发生变化时调用
|
||||
*/
|
||||
public updateAllRobotImageSizes(): void {
|
||||
this.robots.forEach(({ id, type, label }) => {
|
||||
const pen = this.getPenById(id);
|
||||
if (pen && pen.robot) {
|
||||
const imageConfig = this.#mapRobotImage(type, pen.robot.active, label); // 传递机器人名称
|
||||
this.setValue(
|
||||
{
|
||||
id,
|
||||
iconWidth: imageConfig.iconWidth,
|
||||
iconHeight: imageConfig.iconHeight,
|
||||
iconTop: imageConfig.iconTop,
|
||||
},
|
||||
{ render: true, history: false, doEvent: false },
|
||||
);
|
||||
// 同步状态图标尺寸/位置
|
||||
this.updateRobotStatusOverlay(id, true);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新单个机器人的图片
|
||||
* 当机器人自定义图片配置发生变化时调用
|
||||
* @param robotName 机器人名称
|
||||
*/
|
||||
public updateRobotImage(robotName: string): void {
|
||||
const robot = this.robots.find(r => r.label === robotName);
|
||||
if (!robot) return;
|
||||
|
||||
const pen = this.getPenById(robot.id);
|
||||
if (pen && pen.robot) {
|
||||
const imageConfig = this.#mapRobotImage(robot.type, pen.robot.active, robotName);
|
||||
this.setValue(
|
||||
{
|
||||
id: robot.id,
|
||||
image: imageConfig.image,
|
||||
iconWidth: imageConfig.iconWidth,
|
||||
iconHeight: imageConfig.iconHeight,
|
||||
iconTop: imageConfig.iconTop,
|
||||
},
|
||||
{ render: true, history: false, doEvent: false },
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
#mapRobotImage(
|
||||
type: RobotType,
|
||||
active?: boolean,
|
||||
robotName?: string,
|
||||
): Required<Pick<MapPen, 'image' | 'iconWidth' | 'iconHeight' | 'iconTop' | 'canvasLayer'>> {
|
||||
const theme = this.data().theme;
|
||||
|
||||
// 检查是否启用自定义图片且有机器人名称
|
||||
const useCustomImages = colorConfig.getColor('robot.useCustomImages') === 'true';
|
||||
|
||||
let image: string;
|
||||
if (useCustomImages && robotName) {
|
||||
// 尝试获取自定义图片
|
||||
const customImage = colorConfig.getRobotCustomImage(robotName, active ? 'active' : 'normal');
|
||||
if (customImage) {
|
||||
image = customImage;
|
||||
} else {
|
||||
// 如果没有自定义图片,回退到默认图片
|
||||
image = import.meta.env.BASE_URL + (active ? `/robot/${type}-active-${theme}.png` : `/robot/${type}-${theme}.png`);
|
||||
}
|
||||
} else {
|
||||
// 使用默认图片
|
||||
image = import.meta.env.BASE_URL + (active ? `/robot/${type}-active-${theme}.png` : `/robot/${type}-${theme}.png`);
|
||||
}
|
||||
|
||||
// 使用配置的机器人图片尺寸,如果没有配置则使用默认值
|
||||
const iconWidth = colorConfig.getColor('robot.imageWidth') ? Number(colorConfig.getColor('robot.imageWidth')) : 42;
|
||||
const iconHeight = colorConfig.getColor('robot.imageHeight')
|
||||
? Number(colorConfig.getColor('robot.imageHeight'))
|
||||
: 76;
|
||||
|
||||
// 使用优化的像素对齐算法,确保小车和光圈精确重合
|
||||
const iconTop = this.#calculatePixelAlignedOffset(-16);
|
||||
|
||||
return { image, iconWidth, iconHeight, iconTop, canvasLayer: CanvasLayer.CanvasImage };
|
||||
}
|
||||
//#endregion
|
||||
|
||||
/**
|
||||
* 计算机器人状态覆盖图标(优先级:载货 > 不接单 > 充电)
|
||||
*/
|
||||
#getRobotStatusIcon(pen?: MapPen): string | null {
|
||||
if (!pen) return null;
|
||||
// 兼容:状态可能存放在 pen.robot 或 机器人信息表中
|
||||
const r1: any = pen.robot ?? {};
|
||||
const r2 = this.getRobotById(pen.id || '');
|
||||
const toBool = (v: any) => v === true || v === 1 || v === '1';
|
||||
const isCarrying = toBool(r1?.isCarrying ?? r2?.isCarrying);
|
||||
const canOrder = toBool(r1?.canOrder ?? r2?.canOrder);
|
||||
const isCharging = toBool(r1?.isCharging ?? r2?.isCharging);
|
||||
|
||||
if (isCarrying) return cargoIcon;
|
||||
if (!canOrder) return notAcceptingOrdersIcon;
|
||||
if (isCharging) return chargingIcon;
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新单个机器人的状态覆盖图标位置/尺寸/显隐
|
||||
* @param id 机器人ID
|
||||
* @param render 是否立即渲染
|
||||
* @param newPosition 可选,提供新的位置和角度信息,用于解决异步更新时的延迟问题
|
||||
*/
|
||||
public updateRobotStatusOverlay(
|
||||
id: string,
|
||||
render = false,
|
||||
newPosition?: { x: number; y: number; rotate: number },
|
||||
): void {
|
||||
const pen = this.getPenById(id);
|
||||
if (!pen) return;
|
||||
|
||||
const icon = this.#getRobotStatusIcon(pen);
|
||||
const robotVisible = pen.visible !== false;
|
||||
|
||||
// 计算覆盖图标的尺寸(为机器人图片的一半)
|
||||
const baseW = (pen as any).iconWidth ?? 42;
|
||||
const baseH = (pen as any).iconHeight ?? 76;
|
||||
const oW = Math.max(8, Math.floor(baseW * 0.5));
|
||||
const oH = Math.max(8, Math.floor(baseH * 0.5));
|
||||
|
||||
// 以机器人中心为基准,做一个轻微向下的偏移,并随机器人一起旋转
|
||||
const rect = this.getPenRect(pen);
|
||||
const deg: number = newPosition?.rotate ?? pen.rotate ?? 0;
|
||||
const theta = (deg * Math.PI) / 180;
|
||||
// 如果有新位置,使用新位置的 x,y + 容器宽高的一半;否则用旧方法
|
||||
const cx = (newPosition?.x ?? rect.x) + rect.width / 2;
|
||||
const cy = (newPosition?.y ?? rect.y) + rect.height / 2;
|
||||
const iconTop = (pen as any).iconTop ?? 0;
|
||||
// 在本地坐标中的偏移(以机器人中心为原点,y向下为正)
|
||||
const localDx = 0;
|
||||
const localDy = iconTop + 16;
|
||||
// 旋转后的偏移
|
||||
const rotDx = localDx * Math.cos(theta) - localDy * Math.sin(theta);
|
||||
const rotDy = localDx * Math.sin(theta) + localDy * Math.cos(theta);
|
||||
// 覆盖图标中心点
|
||||
const icx = cx + rotDx;
|
||||
const icy = cy + rotDy;
|
||||
const ox = icx - oW / 2;
|
||||
const oy = icy - oH / 2;
|
||||
|
||||
const overlayId = `robot-status-${id}`;
|
||||
const exist = this.getPenById(overlayId);
|
||||
|
||||
// 机器人不可见:隐藏覆盖图标
|
||||
if (!robotVisible) {
|
||||
if (exist) {
|
||||
this.setValue({ id: overlayId, visible: false }, { render, history: false, doEvent: false });
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// 没有状态图标时:如果之前有覆盖图标,则保持现状,不主动隐藏,避免无新数据推送时图标消失
|
||||
if (!icon) {
|
||||
return;
|
||||
}
|
||||
|
||||
// 如果已存在覆盖图标,更新其属性;否则创建新的 image 图元
|
||||
if (exist) {
|
||||
this.setValue(
|
||||
{
|
||||
id: overlayId,
|
||||
image: icon,
|
||||
x: ox,
|
||||
y: oy,
|
||||
width: oW,
|
||||
height: oH,
|
||||
rotate: deg,
|
||||
visible: true,
|
||||
locked: LockState.Disable,
|
||||
},
|
||||
{ render: false, history: false, doEvent: false },
|
||||
);
|
||||
// 置顶,保证覆盖在机器人之上
|
||||
this.top([exist]);
|
||||
if (render) this.render();
|
||||
return;
|
||||
}
|
||||
|
||||
const overlayPen: MapPen = {
|
||||
id: overlayId,
|
||||
name: 'image',
|
||||
tags: ['robot-status'],
|
||||
x: ox,
|
||||
y: oy,
|
||||
width: oW,
|
||||
height: oH,
|
||||
image: icon,
|
||||
rotate: deg,
|
||||
canvasLayer: CanvasLayer.CanvasImage,
|
||||
locked: LockState.Disable,
|
||||
visible: true,
|
||||
} as any;
|
||||
this.addPen(overlayPen, false, false, true);
|
||||
this.top([overlayPen]);
|
||||
if (render) this.render();
|
||||
}
|
||||
|
||||
//#region 点位
|
||||
/** 画布上所有点位对象列表,响应式更新 */
|
||||
public readonly points = useObservable<MapPen[], MapPen[]>(
|
||||
@ -1863,6 +1450,7 @@ export class EditorService extends Meta2d {
|
||||
|
||||
// 初始化库位服务
|
||||
this.storageLocationService = new StorageLocationService(this, '');
|
||||
this.robotService = new EditorRobotService(this);
|
||||
|
||||
// 初始化自动生成库位工具
|
||||
this.autoStorageGenerator = new AutoStorageGenerator(this);
|
||||
@ -1920,7 +1508,7 @@ export class EditorService extends Meta2d {
|
||||
if (!pen.robot?.type) return;
|
||||
// 从pen的text属性获取机器人名称
|
||||
const robotName = pen.text || '';
|
||||
this.canvas.updateValue(pen, this.#mapRobotImage(pen.robot.type, pen.robot.active, robotName));
|
||||
this.canvas.updateValue(pen, this.robotService.mapRobotImage(pen.robot.type, pen.robot.active, robotName));
|
||||
});
|
||||
this.render();
|
||||
}
|
||||
@ -2153,25 +1741,7 @@ export class EditorService extends Meta2d {
|
||||
this.addDrawLineFn('bezier3', lineBezier3);
|
||||
}
|
||||
|
||||
public removeRobotsFromAllLabels(robotIds: string[]): void {
|
||||
robotIds.forEach((robotId) => {
|
||||
const robot = this.getRobotById(robotId);
|
||||
if (robot) {
|
||||
this.updateRobot(robotId, { lid: [] });
|
||||
}
|
||||
});
|
||||
|
||||
const labels = clone(this.#robotLabels$$.value);
|
||||
labels.forEach((label) => {
|
||||
if (label.robots) {
|
||||
remove(label.robots, (id) => robotIds.includes(id));
|
||||
}
|
||||
});
|
||||
this.#robotLabels$$.next(labels);
|
||||
|
||||
(<SceneData>this.store.data).robots = [...this.#robotMap.values()];
|
||||
(<SceneData>this.store.data).robotLabels = this.#robotLabels$$.value;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
//#region 自定义绘制函数
|
||||
@ -2577,75 +2147,6 @@ function drawArea(ctx: CanvasRenderingContext2D, pen: MapPen): void {
|
||||
* - isWaring=true, isFault=false → 告警
|
||||
* - isWaring=false, isFault=false → 正常
|
||||
*/
|
||||
function getRobotStatus(isWaring?: boolean, isFault?: boolean): 'fault' | 'warning' | 'normal' {
|
||||
// 只要 isFault 为 true,无论 isWaring 是什么,都是故障状态
|
||||
if (isFault) return 'fault';
|
||||
// 如果 isFault 为 false 但 isWaring 为 true,则是告警状态
|
||||
if (isWaring) return 'warning';
|
||||
// 两者都为 false 时,为正常状态
|
||||
return 'normal';
|
||||
}
|
||||
|
||||
/**
|
||||
* 绘制机器人的自定义函数
|
||||
* @param ctx Canvas 2D绘制上下文
|
||||
* @param pen 机器人图形对象
|
||||
*/
|
||||
function drawRobot(ctx: CanvasRenderingContext2D, pen: MapPen): void {
|
||||
const theme = sTheme.editor;
|
||||
const { lineWidth: s = 1 } = pen.calculative ?? {};
|
||||
const { x = 0, y = 0, width: w = 0, height: h = 0, rotate: deg = 0 } = pen.calculative?.worldRect ?? {};
|
||||
const { active, path, isWaring, isFault } = pen.robot ?? {};
|
||||
|
||||
if (!active) return;
|
||||
|
||||
// 根据机器人状态获取颜色
|
||||
const status = getRobotStatus(isWaring, isFault);
|
||||
|
||||
// 基于机器人图片大小计算光圈半径,光圈比图片大20%,并随画布缩放
|
||||
const imageWidth = colorConfig.getColor('robot.imageWidth')
|
||||
? Number(colorConfig.getColor('robot.imageWidth')) : 60;
|
||||
const imageHeight = colorConfig.getColor('robot.imageHeight')
|
||||
? Number(colorConfig.getColor('robot.imageHeight')) : 80;
|
||||
// 取图片的较大边作为基准,计算圆形光圈的半径
|
||||
const imageSize = Math.max(imageWidth, imageHeight);
|
||||
const robotRadius = (imageSize * 1.2 * s) / 2; // 光圈半径比图片大20%,并随画布缩放
|
||||
|
||||
const ox = x + w / 2;
|
||||
const oy = y + h / 2;
|
||||
ctx.save();
|
||||
ctx.beginPath();
|
||||
ctx.arc(ox, oy, robotRadius, 0, Math.PI * 2);
|
||||
ctx.fillStyle = colorConfig.getColor(`robot.fill${status === 'normal' ? 'Normal' : status === 'warning' ? 'Warning' : 'Fault'}`) || (get(theme, `robot.fill-${status}`) ?? (get(theme, 'robot.fill') ?? ''));
|
||||
ctx.fill();
|
||||
ctx.strokeStyle = colorConfig.getColor(`robot.stroke${status === 'normal' ? 'Normal' : status === 'warning' ? 'Warning' : 'Fault'}`) || (get(theme, `robot.stroke-${status}`) ?? (get(theme, 'robot.stroke') ?? ''));
|
||||
ctx.stroke();
|
||||
if (path?.length) {
|
||||
ctx.strokeStyle = colorConfig.getColor('robot.line') || (get(theme, 'robot.line') ?? '');
|
||||
ctx.lineCap = 'round';
|
||||
ctx.lineWidth = s * 4;
|
||||
ctx.setLineDash([s * 5, s * 10]);
|
||||
ctx.translate(ox, oy);
|
||||
ctx.rotate((-deg * Math.PI) / 180);
|
||||
ctx.beginPath();
|
||||
ctx.moveTo(0, 0);
|
||||
path.forEach((d) => ctx.lineTo(d.x * s, d.y * s));
|
||||
ctx.stroke();
|
||||
const { x: ex1 = 0, y: ey1 = 0 } = nth(path, -1) ?? {};
|
||||
const { x: ex2 = 0, y: ey2 = 0 } = nth(path, -2) ?? {};
|
||||
const r = Math.atan2(ey1 - ey2, ex1 - ex2) + Math.PI;
|
||||
ctx.setLineDash([0]);
|
||||
ctx.translate(ex1 * s, ey1 * s);
|
||||
ctx.beginPath();
|
||||
ctx.moveTo(Math.cos(r + Math.PI / 4) * s * 10, Math.sin(r + Math.PI / 4) * s * 10);
|
||||
ctx.lineTo(0, 0);
|
||||
ctx.lineTo(Math.cos(r - Math.PI / 4) * s * 10, Math.sin(r - Math.PI / 4) * s * 10);
|
||||
ctx.stroke();
|
||||
ctx.setTransform(1, 0, 0, 1, 0, 0);
|
||||
}
|
||||
ctx.restore();
|
||||
}
|
||||
|
||||
//#endregion
|
||||
|
||||
//#region 辅助函数
|
||||
|
Loading…
x
Reference in New Issue
Block a user