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 Point,
|
||||||
type Rect,
|
type Rect,
|
||||||
} from '@api/map';
|
} from '@api/map';
|
||||||
import type { RobotGroup, RobotInfo, RobotLabel,RobotType } from '@api/robot';
|
import type { RobotGroup, RobotInfo, RobotLabel } from '@api/robot';
|
||||||
import type {
|
import type {
|
||||||
GroupSceneDetail,
|
GroupSceneDetail,
|
||||||
SceneData,
|
SceneData,
|
||||||
@ -23,14 +23,11 @@ import type {
|
|||||||
import sTheme from '@core/theme.service';
|
import sTheme from '@core/theme.service';
|
||||||
import { CanvasLayer, LockState, Meta2d, type Meta2dStore, type Pen, s8 } from '@meta2d/core';
|
import { CanvasLayer, LockState, Meta2d, type Meta2dStore, type Pen, s8 } from '@meta2d/core';
|
||||||
import { useObservable } from '@vueuse/rxjs';
|
import { useObservable } from '@vueuse/rxjs';
|
||||||
import { clone, get, isEmpty, isNil, isString, nth, pick, remove, some } from 'lodash-es';
|
import { clone, get, isEmpty, isNil, isString, pick, } from 'lodash-es';
|
||||||
import { BehaviorSubject, debounceTime, filter, map, Subject, switchMap } from 'rxjs';
|
import { debounceTime, filter, map, Subject, switchMap } from 'rxjs';
|
||||||
import { reactive, watch } from 'vue';
|
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 { AreaOperationService } from './area-operation.service';
|
||||||
import { BinTaskManagerService } from './bintask-manager.service';
|
import { BinTaskManagerService } from './bintask-manager.service';
|
||||||
import colorConfig from './color/color-config.service';
|
import colorConfig from './color/color-config.service';
|
||||||
@ -39,6 +36,7 @@ import {
|
|||||||
drawStorageLocation,
|
drawStorageLocation,
|
||||||
drawStorageMore,
|
drawStorageMore,
|
||||||
} from './draw/storage-location-drawer';
|
} from './draw/storage-location-drawer';
|
||||||
|
import { drawRobot,EditorRobotService } from './editor-robot.service';
|
||||||
import { LayerManagerService } from './layer-manager.service';
|
import { LayerManagerService } from './layer-manager.service';
|
||||||
import { createStorageLocationUpdater,StorageLocationService } from './storage-location.service';
|
import { createStorageLocationUpdater,StorageLocationService } from './storage-location.service';
|
||||||
import { AutoStorageGenerator } from './utils/auto-storage-generator';
|
import { AutoStorageGenerator } from './utils/auto-storage-generator';
|
||||||
@ -59,6 +57,7 @@ export class EditorService extends Meta2d {
|
|||||||
private readonly layerManager: LayerManagerService;
|
private readonly layerManager: LayerManagerService;
|
||||||
/** 库位服务实例 */
|
/** 库位服务实例 */
|
||||||
private readonly storageLocationService: StorageLocationService;
|
private readonly storageLocationService: StorageLocationService;
|
||||||
|
private readonly robotService: EditorRobotService;
|
||||||
|
|
||||||
/** 区域操作服务实例 */
|
/** 区域操作服务实例 */
|
||||||
private readonly areaOperationService!: AreaOperationService;
|
private readonly areaOperationService!: AreaOperationService;
|
||||||
@ -94,14 +93,13 @@ export class EditorService extends Meta2d {
|
|||||||
|
|
||||||
this.open();
|
this.open();
|
||||||
this.setState(editable);
|
this.setState(editable);
|
||||||
this.#loadRobots(robotGroups, robots);
|
this.robotService.loadInitialData(robotGroups, robots, robotLabels);
|
||||||
this.#loadLabels(robotLabels);
|
|
||||||
await this.#loadScenePoints(points, isImport);
|
await this.#loadScenePoints(points, isImport);
|
||||||
this.#loadSceneRoutes(routes, isImport);
|
this.#loadSceneRoutes(routes, isImport);
|
||||||
await this.#loadSceneAreas(areas, isImport);
|
await this.#loadSceneAreas(areas, isImport);
|
||||||
|
|
||||||
// 确保正确的层级顺序:路线 < 点位 < 机器人
|
// 确保正确的层级顺序:路线 < 点位 < 机器人
|
||||||
this.#ensureCorrectLayerOrder();
|
this.ensureCorrectLayerOrder();
|
||||||
|
|
||||||
// 为所有动作点创建库位pen对象
|
// 为所有动作点创建库位pen对象
|
||||||
this.createAllStorageLocationPens();
|
this.createAllStorageLocationPens();
|
||||||
@ -201,14 +199,8 @@ export class EditorService extends Meta2d {
|
|||||||
* @param groups 机器人组列表
|
* @param groups 机器人组列表
|
||||||
* @param robots 机器人信息列表
|
* @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 标准场景点位数据数组
|
* @param points 标准场景点位数据数组
|
||||||
@ -410,7 +402,7 @@ export class EditorService extends Meta2d {
|
|||||||
properties,
|
properties,
|
||||||
};
|
};
|
||||||
if ([MapPointType.充电点, MapPointType.停靠点].includes(type)) {
|
if ([MapPointType.充电点, MapPointType.停靠点].includes(type)) {
|
||||||
point.robots = robots?.filter((v) => this.#robotMap.has(v));
|
point.robots = robots?.filter((v) => this.robotService.hasRobot(v));
|
||||||
// 若未提供 enabled,则默认启用
|
// 若未提供 enabled,则默认启用
|
||||||
point.enabled = (enabled ?? 1) as 0 | 1;
|
point.enabled = (enabled ?? 1) as 0 | 1;
|
||||||
}
|
}
|
||||||
@ -526,7 +518,7 @@ export class EditorService extends Meta2d {
|
|||||||
* 确保正确的层级顺序
|
* 确保正确的层级顺序
|
||||||
* 委托给图层管理服务处理
|
* 委托给图层管理服务处理
|
||||||
*/
|
*/
|
||||||
#ensureCorrectLayerOrder(): void {
|
public ensureCorrectLayerOrder(): void {
|
||||||
this.layerManager.ensureCorrectLayerOrder();
|
this.layerManager.ensureCorrectLayerOrder();
|
||||||
}
|
}
|
||||||
//#endregion
|
//#endregion
|
||||||
@ -579,184 +571,94 @@ export class EditorService extends Meta2d {
|
|||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|
||||||
//#region 机器人组管理
|
//#region 机器人服务
|
||||||
/** 机器人信息映射表,响应式存储所有机器人数据 */
|
|
||||||
readonly #robotMap = reactive<Map<RobotInfo['id'], RobotInfo>>(new Map());
|
|
||||||
|
|
||||||
/** 获取所有机器人信息数组 */
|
|
||||||
public get robots(): RobotInfo[] {
|
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 {
|
public checkRobotById(id: RobotInfo['id']): boolean {
|
||||||
return this.#robotMap.has(id);
|
return this.robotService.hasRobot(id);
|
||||||
}
|
}
|
||||||
|
|
||||||
public getRobotById(id: RobotInfo['id']): RobotInfo | undefined {
|
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 {
|
public updateRobot(id: RobotInfo['id'], value: Partial<RobotInfo>): void {
|
||||||
const robot = this.getRobotById(id);
|
this.robotService.updateRobot(id, value);
|
||||||
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()];
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public addRobots(gid: RobotInfo['gid'], robots: RobotInfo[]): void {
|
public addRobots(gid: RobotInfo['gid'], robots: RobotInfo[]): void {
|
||||||
const groups = clone(this.#robotGroups$$.value);
|
this.robotService.addRobots(gid, robots);
|
||||||
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;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public removeRobots(ids: RobotInfo['id'][]): void {
|
public removeRobots(ids: RobotInfo['id'][]): void {
|
||||||
ids?.forEach((v) => this.#robotMap.delete(v));
|
this.robotService.removeRobots(ids);
|
||||||
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;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public updateRobots(ids: RobotInfo['id'][], value: Partial<RobotInfo>): void {
|
public updateRobots(ids: RobotInfo['id'][], value: Partial<RobotInfo>): void {
|
||||||
ids?.forEach((v) => {
|
this.robotService.updateRobots(ids, value);
|
||||||
const robot = this.#robotMap.get(v);
|
|
||||||
if (isNil(robot)) return;
|
|
||||||
this.#robotMap.set(v, { ...robot, ...value });
|
|
||||||
});
|
|
||||||
(<SceneData>this.store.data).robots = [...this.#robotMap.values()];
|
|
||||||
}
|
}
|
||||||
|
|
||||||
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 {
|
public createRobotGroup(): void {
|
||||||
const id = s8();
|
this.robotService.createRobotGroup();
|
||||||
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;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public deleteRobotGroup(id: RobotGroup['id']): void {
|
public deleteRobotGroup(id: RobotGroup['id']): void {
|
||||||
const groups = clone(this.#robotGroups$$.value);
|
this.robotService.deleteRobotGroup(id);
|
||||||
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;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public updateRobotGroupLabel(id: RobotGroup['id'], label: RobotGroup['label']): void {
|
public updateRobotGroupLabel(id: RobotGroup['id'], label: RobotGroup['label']): void {
|
||||||
const groups = this.#robotGroups$$.value;
|
this.robotService.updateRobotGroupLabel(id, label);
|
||||||
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;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public createRobotLabel(): void {
|
public createRobotLabel(): void {
|
||||||
const id = s8();
|
this.robotService.createRobotLabel();
|
||||||
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;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public deleteRobotLabel(id: RobotLabel['id']): void {
|
public deleteRobotLabel(id: RobotLabel['id']): void {
|
||||||
const labels = clone(this.#robotLabels$$.value);
|
this.robotService.deleteRobotLabel(id);
|
||||||
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;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public updateRobotLabel(id: RobotLabel['id'], labelName: RobotLabel['label']): void {
|
public updateRobotLabel(id: RobotLabel['id'], labelName: RobotLabel['label']): void {
|
||||||
const labels = this.#robotLabels$$.value;
|
this.robotService.updateRobotLabel(id, labelName);
|
||||||
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;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public addRobotsToLabel(lid: RobotLabel['id'], robots: RobotInfo[]): void {
|
public addRobotsToLabel(lid: RobotLabel['id'], robots: RobotInfo[]): void {
|
||||||
const labels = clone(this.#robotLabels$$.value);
|
this.robotService.addRobotsToLabel(lid, robots);
|
||||||
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;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public removeRobotFromLabel(labelId: string, robotId: string): void {
|
public removeRobotFromLabel(labelId: string, robotId: string): void {
|
||||||
const labels = clone(this.#robotLabels$$.value);
|
this.robotService.removeRobotFromLabel(labelId, robotId);
|
||||||
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);
|
public removeRobotsFromAllLabels(robotIds: string[]): void {
|
||||||
if (robot?.lid) {
|
this.robotService.removeRobotsFromAllLabels(robotIds);
|
||||||
const newLid = robot.lid.filter((id) => id !== labelId);
|
}
|
||||||
this.updateRobot(robotId, { lid: newLid });
|
|
||||||
}
|
|
||||||
|
|
||||||
(<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
|
//#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>();
|
readonly #change$$ = new Subject<boolean>();
|
||||||
@ -1022,7 +887,6 @@ export class EditorService extends Meta2d {
|
|||||||
return createStorageLocationUpdater(this.storageLocationService);
|
return createStorageLocationUpdater(this.storageLocationService);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 为所有动作点创建库位pen对象
|
* 为所有动作点创建库位pen对象
|
||||||
* 用于初始化或刷新所有动作点的库位显示
|
* 用于初始化或刷新所有动作点的库位显示
|
||||||
@ -1080,8 +944,6 @@ export class EditorService extends Meta2d {
|
|||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 根据设备ID更新自动门点状态
|
* 根据设备ID更新自动门点状态
|
||||||
* @param deviceId 设备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 点位
|
//#region 点位
|
||||||
/** 画布上所有点位对象列表,响应式更新 */
|
/** 画布上所有点位对象列表,响应式更新 */
|
||||||
public readonly points = useObservable<MapPen[], MapPen[]>(
|
public readonly points = useObservable<MapPen[], MapPen[]>(
|
||||||
@ -1863,6 +1450,7 @@ export class EditorService extends Meta2d {
|
|||||||
|
|
||||||
// 初始化库位服务
|
// 初始化库位服务
|
||||||
this.storageLocationService = new StorageLocationService(this, '');
|
this.storageLocationService = new StorageLocationService(this, '');
|
||||||
|
this.robotService = new EditorRobotService(this);
|
||||||
|
|
||||||
// 初始化自动生成库位工具
|
// 初始化自动生成库位工具
|
||||||
this.autoStorageGenerator = new AutoStorageGenerator(this);
|
this.autoStorageGenerator = new AutoStorageGenerator(this);
|
||||||
@ -1920,7 +1508,7 @@ export class EditorService extends Meta2d {
|
|||||||
if (!pen.robot?.type) return;
|
if (!pen.robot?.type) return;
|
||||||
// 从pen的text属性获取机器人名称
|
// 从pen的text属性获取机器人名称
|
||||||
const robotName = 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();
|
this.render();
|
||||||
}
|
}
|
||||||
@ -2153,25 +1741,7 @@ export class EditorService extends Meta2d {
|
|||||||
this.addDrawLineFn('bezier3', lineBezier3);
|
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 自定义绘制函数
|
//#region 自定义绘制函数
|
||||||
@ -2577,75 +2147,6 @@ function drawArea(ctx: CanvasRenderingContext2D, pen: MapPen): void {
|
|||||||
* - isWaring=true, isFault=false → 告警
|
* - isWaring=true, isFault=false → 告警
|
||||||
* - isWaring=false, 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
|
//#endregion
|
||||||
|
|
||||||
//#region 辅助函数
|
//#region 辅助函数
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user