This commit is contained in:
chndfang 2025-05-25 00:07:22 +08:00
parent 6930e2952c
commit d6c15c2235
27 changed files with 628 additions and 1138 deletions

1455
package-lock.json generated

File diff suppressed because it is too large Load Diff

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.7 KiB

BIN
public/robot/1-dark.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.6 KiB

BIN
public/robot/1-light.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.9 KiB

BIN
public/robot/2-dark.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.7 KiB

BIN
public/robot/2-light.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.9 KiB

BIN
public/robot/3-dark.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.7 KiB

BIN
public/robot/3-light.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.7 KiB

View File

@ -62,7 +62,7 @@ export const EDITOR_CONFIG: Options = {
disableAnchor: true, disableAnchor: true,
disableEmptyLine: true, disableEmptyLine: true,
disableRepeatLine: true, disableRepeatLine: true,
minScale: 0.19, minScale: 0.24,
maxScale: 4.01, maxScale: 4.01,
scaleOff: 0.01, scaleOff: 0.01,
defaultAnchors: [], defaultAnchors: [],
@ -71,4 +71,7 @@ export const EDITOR_CONFIG: Options = {
fontSize: 14, fontSize: 14,
lineHeight: 1.5, lineHeight: 1.5,
fontFamily: 'system-ui', fontFamily: 'system-ui',
textRotate: false,
textAlign: 'center',
textBaseline: 'top',
}; };

View File

@ -1,4 +1,4 @@
import type { RobotInfo } from '@api/robot'; import type { RobotInfo, RobotRealtimeInfo } from '@api/robot';
import type { Pen } from '@meta2d/core'; import type { Pen } from '@meta2d/core';
import type { MapAreaType, MapPointType, MapRoutePassType, MapRouteType } from './constant'; import type { MapAreaType, MapPointType, MapRoutePassType, MapRouteType } from './constant';
@ -10,6 +10,7 @@ export interface MapPen extends Pen {
point?: MapPointInfo; // 点位信息 point?: MapPointInfo; // 点位信息
route?: MapRouteInfo; // 线路信息 route?: MapRouteInfo; // 线路信息
area?: MapAreaInfo; // 区域信息 area?: MapAreaInfo; // 区域信息
robot?: MapRobotInfo; // 实时机器人信息
attrs?: Record<string, unknown>; // 额外属性 attrs?: Record<string, unknown>; // 额外属性
activeAttrs?: Array<string>; // 已激活的额外属性 activeAttrs?: Array<string>; // 已激活的额外属性
@ -45,6 +46,10 @@ export interface MapAreaInfo {
} }
//#endregion //#endregion
//#region 机器人
export type MapRobotInfo = Pick<RobotRealtimeInfo, 'type' | 'active' | 'path'>;
//#endregion
export type Point = Record<'x' | 'y', number>; export type Point = Record<'x' | 'y', number>;
export type Rect = Record<'x' | 'y' | 'width' | 'height', number>; export type Rect = Record<'x' | 'y' | 'width' | 'height', number>;
export type AnchorPosition = 't' | 'b' | 'l' | 'r'; export type AnchorPosition = 't' | 'b' | 'l' | 'r';

View File

@ -30,3 +30,11 @@ export interface RobotDetail extends RobotInfo {
taskBattery?: number; // 任务电量 taskBattery?: number; // 任务电量
swapBattery?: number; // 交换电量 swapBattery?: number; // 交换电量
} }
export interface RobotRealtimeInfo extends RobotInfo {
x: number; // 坐标x
y: number; // 坐标y
active?: boolean; // 是否运行
angle?: number; // 旋转角度
path?: Array<[number, number]>; // 规划路径
}

View File

@ -1,5 +1,6 @@
import type { RobotGroup } from '@api/robot'; import type { RobotGroup } from '@api/robot';
import http from '@core/http'; import http from '@core/http';
import ws from '@core/ws';
import type { GroupSceneDetail, SceneDetail, SceneInfo } from './type'; import type { GroupSceneDetail, SceneDetail, SceneInfo } from './type';
@ -10,6 +11,8 @@ const enum API {
= '/scene/getByGroupId', = '/scene/getByGroupId',
= '/scene/saveByGroupId', = '/scene/saveByGroupId',
= '/scene/monitor/:id',
} }
export async function getSceneById(id: SceneInfo['id']): Promise<SceneDetail | null> { export async function getSceneById(id: SceneInfo['id']): Promise<SceneDetail | null> {
@ -84,3 +87,14 @@ export async function saveSceneByGroupId(id: RobotGroup['id'], sid: RobotGroup['
return false; return false;
} }
} }
export async function monitorSceneById(id: SceneInfo['id']): Promise<WebSocket | null> {
if (!id) return null;
try {
const socket = await ws.create(API..replace(':id', id));
return socket;
} catch (error) {
console.debug(error);
return null;
}
}

View File

@ -27,5 +27,10 @@
"stroke-12": "#0DBB8A99", "stroke-12": "#0DBB8A99",
"fill-12": "#0DBB8A33", "fill-12": "#0DBB8A33",
"strokeActive": "#FCC947" "strokeActive": "#FCC947"
},
"robot": {
"stroke": "#01FDAF99",
"fill": "#01FAAD33",
"line": "#01fdaf"
} }
} }

View File

@ -27,5 +27,10 @@
"stroke-12": "#0DBB8A99", "stroke-12": "#0DBB8A99",
"fill-12": "#0DBB8A33", "fill-12": "#0DBB8A33",
"strokeActive": "#EBB214" "strokeActive": "#EBB214"
},
"robot": {
"stroke": "#01FDAF99",
"fill": "#01FAAD33",
"line": "#01fdaf"
} }
} }

View File

@ -46,6 +46,7 @@ const canDelete = computed<boolean>(() => editor.value.current.value?.name === '
class="icon-btn tool-btn" class="icon-btn tool-btn"
:class="{ active: mode === MapAreaType.库区 }" :class="{ active: mode === MapAreaType.库区 }"
size="large" size="large"
:title="$t('添加库区')"
@click="mode = MapAreaType.库区" @click="mode = MapAreaType.库区"
> >
<i class="icon" :class="mode === MapAreaType.库区 ? 'area1-active' : 'area1'" /> <i class="icon" :class="mode === MapAreaType.库区 ? 'area1-active' : 'area1'" />
@ -54,6 +55,7 @@ const canDelete = computed<boolean>(() => editor.value.current.value?.name === '
class="icon-btn tool-btn ml-12" class="icon-btn tool-btn ml-12"
:class="{ active: mode === MapAreaType.互斥区 }" :class="{ active: mode === MapAreaType.互斥区 }"
size="large" size="large"
:title="$t('添加互斥区')"
@click="mode = MapAreaType.互斥区" @click="mode = MapAreaType.互斥区"
> >
<i class="icon" :class="mode === MapAreaType.互斥区 ? 'area11-active' : 'area11'" /> <i class="icon" :class="mode === MapAreaType.互斥区 ? 'area11-active' : 'area11'" />
@ -62,6 +64,7 @@ const canDelete = computed<boolean>(() => editor.value.current.value?.name === '
class="icon-btn tool-btn ml-12" class="icon-btn tool-btn ml-12"
:class="{ active: mode === MapAreaType.非互斥区 }" :class="{ active: mode === MapAreaType.非互斥区 }"
size="large" size="large"
:title="$t('添加非互斥区')"
@click="mode = MapAreaType.非互斥区" @click="mode = MapAreaType.非互斥区"
> >
<i class="icon" :class="mode === MapAreaType.非互斥区 ? 'area12-active' : 'area12'" /> <i class="icon" :class="mode === MapAreaType.非互斥区 ? 'area12-active' : 'area12'" />
@ -69,18 +72,19 @@ const canDelete = computed<boolean>(() => editor.value.current.value?.name === '
<a-divider class="size-24 mh-8" type="vertical" /> <a-divider class="size-24 mh-8" type="vertical" />
<a-button class="icon-btn tool-btn" size="large" @click="updateScene"> <a-button class="icon-btn tool-btn" size="large" :title="$t('保存')" @click="updateScene">
<i class="mask save" /> <i class="mask save" />
</a-button> </a-button>
<a-button class="icon-btn tool-btn ml-12" size="large" @click="editor.undo()"> <a-button class="icon-btn tool-btn ml-12" size="large" :title="$t('撤销')" @click="editor.undo()">
<i class="mask undo" /> <i class="mask undo" />
</a-button> </a-button>
<a-button class="icon-btn tool-btn ml-12" size="large" @click="editor.redo()"> <a-button class="icon-btn tool-btn ml-12" size="large" :title="$t('重做')" @click="editor.redo()">
<i class="mask redo" /> <i class="mask redo" />
</a-button> </a-button>
<a-button <a-button
class="icon-btn tool-btn ml-12" class="icon-btn tool-btn ml-12"
size="large" size="large"
:title="$t('删除区域')"
@click="editor.deleteById(editor.current.value?.id)" @click="editor.deleteById(editor.current.value?.id)"
:disabled="!canDelete" :disabled="!canDelete"
> >

View File

@ -14,6 +14,7 @@ import { useRouter } from 'vue-router';
type Props = { type Props = {
token: InjectionKey<ShallowRef<EditorService>>; token: InjectionKey<ShallowRef<EditorService>>;
sid: string; sid: string;
showGroupEdit?: boolean;
editable?: boolean; editable?: boolean;
current?: string; current?: string;
}; };
@ -149,8 +150,8 @@ const toRemoveRobots = () =>
<a-space align="center"> <a-space align="center">
<a-button <a-button
class="icon-btn panel-btn" class="icon-btn panel-btn"
:title="$t('抢占控制权')"
size="small" size="small"
:title="$t('抢占控制权')"
@click="seizeRobots" @click="seizeRobots"
:disabled="!selected.size" :disabled="!selected.size"
> >
@ -158,8 +159,8 @@ const toRemoveRobots = () =>
</a-button> </a-button>
<a-button <a-button
class="icon-btn panel-btn" class="icon-btn panel-btn"
:title="$t('移除机器人')"
size="small" size="small"
:title="$t('移除机器人')"
@click="toRemoveRobots" @click="toRemoveRobots"
:disabled="!selected.size" :disabled="!selected.size"
> >
@ -221,7 +222,13 @@ const toRemoveRobots = () =>
/> />
<span>{{ label }}</span> <span>{{ label }}</span>
</a-space> </a-space>
<a-button v-if="!editable" class="open-group-btn icon-btn" size="small" @click.stop="toEditGroup(id)"> <a-button
v-if="showGroupEdit && !editable"
class="open-group-btn icon-btn"
size="small"
:title="$t('编辑组文件')"
@click.stop="toEditGroup(id)"
>
<i class="icon edit_group" /> <i class="icon edit_group" />
</a-button> </a-button>
</a-flex> </a-flex>
@ -237,8 +244,8 @@ const toRemoveRobots = () =>
<template v-if="editable" #actions> <template v-if="editable" #actions>
<a-button <a-button
class="icon-btn panel-btn" class="icon-btn panel-btn"
:title="$t('移除机器人')"
size="small" size="small"
:title="$t('移除机器人')"
@click.stop="toRemoveRobot(item.id)" @click.stop="toRemoveRobot(item.id)"
> >
<i class="icon trash_fill" /> <i class="icon trash_fill" />

View File

@ -1,7 +1,9 @@
<script setup lang="ts"> <script setup lang="ts">
import { getSceneByGroupId, getSceneById } from '@api/scene'; import type { RobotRealtimeInfo } from '@api/robot';
import { getSceneByGroupId, getSceneById, monitorSceneById } from '@api/scene';
import { EditorService } from '@core/editor.service'; import { EditorService } from '@core/editor.service';
import { computed, onMounted, provide, ref, shallowRef, watch } from 'vue'; import { isNil } from 'lodash-es';
import { computed, onMounted, onUnmounted, provide, ref, shallowRef, watch } from 'vue';
const EDITOR_KEY = Symbol('editor-key'); const EDITOR_KEY = Symbol('editor-key');
@ -17,14 +19,26 @@ const readScene = async () => {
title.value = res?.label ?? ''; title.value = res?.label ?? '';
editor.value?.load(res?.json); editor.value?.load(res?.json);
}; };
const monitorScene = async () => {
client.value?.close();
const ws = await monitorSceneById(props.sid);
if (isNil(ws)) return;
ws.onmessage = (e) => {
const { id, x, y, active, angle, path, ...rest } = <RobotRealtimeInfo>JSON.parse(e.data || '{}');
if (!editor.value?.checkRobotById(id)) return;
editor.value?.updateRobot(id, rest);
if (isNil(x) || isNil(y)) {
editor.value.updatePen(id, { visible: false });
} else {
editor.value.refreshRobot(id, { x, y, active, angle, path });
}
};
client.value = ws;
};
//#endregion //#endregion
const title = ref<string>(''); const title = ref<string>('');
watch(
() => props.id,
() => readScene(),
{ immediate: true, flush: 'post' },
);
const container = shallowRef<HTMLDivElement>(); const container = shallowRef<HTMLDivElement>();
const editor = shallowRef<EditorService>(); const editor = shallowRef<EditorService>();
@ -33,6 +47,34 @@ onMounted(() => {
editor.value = new EditorService(container.value!); editor.value = new EditorService(container.value!);
}); });
const client = shallowRef<WebSocket>();
onMounted(async () => {
await readScene();
await editor.value?.initRobots();
// await monitorScene();
const id = 'mock-robot-1';
let x = 800;
let y = 500;
const active = true;
const angle = 0;
const path = <[number, number][]>[
[600, 500],
// [100, 400],
];
editor.value?.refreshRobot(id, { x, y, active, angle, path });
const test = () =>
requestAnimationFrame(() => {
x -= 0.1;
editor.value?.refreshRobot(id, { x, y });
test();
});
// test();
});
onUnmounted(() => {
client.value?.close();
});
const show = ref<boolean>(true); const show = ref<boolean>(true);
const current = ref<{ type: 'robot' | 'point' | 'line' | 'area'; id: string }>(); const current = ref<{ type: 'robot' | 'point' | 'line' | 'area'; id: string }>();
watch( watch(
@ -85,6 +127,18 @@ const selectRobot = (id: string) => {
</a-layout-content> </a-layout-content>
</a-layout> </a-layout>
</a-layout> </a-layout>
<template v-if="current?.id">
<a-float-button style="top: 80px; right: 16px" shape="square" @click="show = !show">
<template #icon><i class="icon detail" /></template>
</a-float-button>
<div v-if="show" class="card-container">
<RobotDetailCard v-if="isRobot" :token="EDITOR_KEY" :current="current.id" />
<PointDetailCard v-if="isPoint" :token="EDITOR_KEY" :current="current.id" />
<RouteDetailCard v-if="isRoute" :token="EDITOR_KEY" :current="current.id" />
<AreaDetailCard v-if="isArea" :token="EDITOR_KEY" :current="current.id" />
</div>
</template>
</template> </template>
<style scoped lang="scss"> <style scoped lang="scss">

View File

@ -130,6 +130,7 @@ const selectRobot = (id: string) => {
:editable="editable" :editable="editable"
:current="current?.id" :current="current?.id"
@change="selectRobot" @change="selectRobot"
show-group-edit
/> />
</a-tab-pane> </a-tab-pane>
<a-tab-pane key="2" :tab="$t('库区')"> <a-tab-pane key="2" :tab="$t('库区')">

View File

@ -10,7 +10,7 @@ import {
MapRouteType, MapRouteType,
type Point, type Point,
} from '@api/map'; } from '@api/map';
import type { RobotGroup, RobotInfo } from '@api/robot'; import type { RobotGroup, RobotInfo, RobotRealtimeInfo, RobotType } from '@api/robot';
import type { import type {
GroupSceneDetail, GroupSceneDetail,
SceneData, SceneData,
@ -22,7 +22,7 @@ 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, pick, remove, some } from 'lodash-es'; import { clone, get, isEmpty, isNil, isString, nth, omitBy, pick, remove, some } from 'lodash-es';
import { BehaviorSubject, debounceTime, filter, map, Subject, switchMap } from 'rxjs'; import { BehaviorSubject, debounceTime, filter, map, Subject, switchMap } from 'rxjs';
import { reactive, watch } from 'vue'; import { reactive, watch } from 'vue';
@ -213,16 +213,26 @@ export class EditorService extends Meta2d {
), ),
); );
//#region 机器人 //#region 机器人
readonly #robotMap = reactive<Map<RobotInfo['id'], RobotInfo>>(new Map()); readonly #robotMap = reactive<Map<RobotInfo['id'], RobotInfo>>(new Map());
public get robots(): RobotInfo[] { public get robots(): RobotInfo[] {
return Array.from(this.#robotMap.values()); return Array.from(this.#robotMap.values());
} }
public checkRobotById(id: RobotInfo['id']): boolean {
return this.#robotMap.has(id);
}
public getRobotById(id: RobotInfo['id']): RobotInfo | undefined { public getRobotById(id: RobotInfo['id']): RobotInfo | undefined {
return this.#robotMap.get(id); 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 });
(<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); const groups = clone(this.#robotGroups$$.value);
const group = groups.find((v) => v.id === gid); const group = groups.find((v) => v.id === gid);
@ -245,11 +255,11 @@ export class EditorService extends Meta2d {
(<SceneData>this.store.data).robots = [...this.#robotMap.values()]; (<SceneData>this.store.data).robots = [...this.#robotMap.values()];
(<SceneData>this.store.data).robotGroups = this.#robotGroups$$.value; (<SceneData>this.store.data).robotGroups = this.#robotGroups$$.value;
} }
public updateRobots(ids: RobotInfo['id'][], values: Partial<RobotInfo>): void { public updateRobots(ids: RobotInfo['id'][], value: Partial<RobotInfo>): void {
ids?.forEach((v) => { ids?.forEach((v) => {
const robot = this.#robotMap.get(v); const robot = this.#robotMap.get(v);
if (isNil(robot)) return; if (isNil(robot)) return;
this.#robotMap.set(v, { ...robot, ...values }); this.#robotMap.set(v, { ...robot, ...value });
}); });
(<SceneData>this.store.data).robots = [...this.#robotMap.values()]; (<SceneData>this.store.data).robots = [...this.#robotMap.values()];
} }
@ -328,8 +338,6 @@ export class EditorService extends Meta2d {
public deleteById(id?: string): void { public deleteById(id?: string): void {
const pen = this.getPenById(id); const pen = this.getPenById(id);
console.log(pen);
if (pen?.name !== 'area') return; if (pen?.name !== 'area') return;
this.delete([pen], true, true); this.delete([pen], true, true);
} }
@ -338,6 +346,61 @@ export class EditorService extends Meta2d {
this.setValue({ ...pen, id }, { render: true, history: record, doEvent: true }); this.setValue({ ...pen, id }, { render: true, history: record, doEvent: true });
} }
//#region 实时机器人
public async initRobots(): Promise<void> {
await Promise.all(
this.robots.map(async ({ id, label, type }) => {
const pen: MapPen = {
...this.#mapRobotImage(type, true),
id,
name: 'robot',
tags: ['robot'],
x: 0,
y: 0,
width: 74,
height: 74,
lineWidth: 1,
robot: { type },
visible: false,
text: label,
textTop: -24,
whiteSpace: 'nowrap',
ellipsis: false,
locked: LockState.Disable,
};
await this.addPen(pen, false, true, true);
}),
);
}
public refreshRobot(id: RobotInfo['id'], info: Partial<RobotRealtimeInfo>): void {
const { rotate: or, robot } = this.getPenById(id) ?? {};
if (!robot?.type) return;
const { x: ex = 37, y: ey = 37, active, angle, path } = info;
const x = ex - 37;
const y = ey - 37;
const rotate = angle ?? or;
const o = { ...robot, ...omitBy({ active, path }, isNil) };
if (isNil(active)) {
this.setValue({ id, x, y, rotate, robot: o, visible: true }, { render: true, history: false, doEvent: false });
} else {
this.setValue(
{ id, ...this.#mapRobotImage(robot.type, active), x, y, rotate, robot: o, visible: true },
{ render: true, history: false, doEvent: false },
);
}
}
#mapRobotImage(
type: RobotType,
active?: boolean,
): Required<Pick<MapPen, 'image' | 'iconWidth' | 'iconHeight' | 'iconTop'>> {
const theme = this.data().theme;
const image = active ? `/robot/${type}-active-${theme}.png` : `/robot/${type}-${theme}.png`;
return { image, iconWidth: 34, iconHeight: 54, iconTop: -5 };
}
//#endregion
//#region 点位 //#region 点位
public readonly points = useObservable<MapPen[], MapPen[]>( public readonly points = useObservable<MapPen[], MapPen[]>(
this.#change$$.pipe( this.#change$$.pipe(
@ -348,7 +411,7 @@ export class EditorService extends Meta2d {
{ initialValue: new Array<MapPen>() }, { initialValue: new Array<MapPen>() },
); );
public async addPoint(p: Point, type = MapPointType., id?: string): Promise<MapPen> { public async addPoint(p: Point, type = MapPointType., id?: string): Promise<void> {
id ||= s8(); id ||= s8();
const pen: MapPen = { const pen: MapPen = {
...p, ...p,
@ -360,7 +423,7 @@ export class EditorService extends Meta2d {
label: `P${id}`, label: `P${id}`,
point: { type }, point: { type },
}; };
return await this.addPen(pen, false, true, true); await this.addPen(pen, false, true, true);
} }
public updatePoint(id: string, info: Partial<MapPointInfo>): void { public updatePoint(id: string, info: Partial<MapPointInfo>): void {
@ -526,11 +589,17 @@ export class EditorService extends Meta2d {
#load(theme: string): void { #load(theme: string): void {
this.setTheme(theme); this.setTheme(theme);
this.setOptions({ color: get(sTheme.editor, 'color') });
this.find('point').forEach((pen) => { this.find('point').forEach((pen) => {
if (!pen.point?.type) return; if (!pen.point?.type) return;
if (pen.point.type < 10) return; if (pen.point.type < 10) return;
this.canvas.updateValue(pen, this.#mapPointImage(pen.point.type)); this.canvas.updateValue(pen, this.#mapPointImage(pen.point.type));
}); });
this.find('robot').forEach((pen) => {
if (!pen.robot?.type) return;
this.canvas.updateValue(pen, this.#mapRobotImage(pen.robot.type, pen.robot.active));
});
this.render(); this.render();
} }
@ -587,7 +656,7 @@ export class EditorService extends Meta2d {
#register() { #register() {
this.register({ line: () => new Path2D() }); this.register({ line: () => new Path2D() });
this.registerCanvasDraw({ point: drawPoint, line: drawLine, area: drawArea }); this.registerCanvasDraw({ point: drawPoint, line: drawLine, area: drawArea, robot: drawRobot });
this.registerAnchors({ point: anchorPoint }); this.registerAnchors({ point: anchorPoint });
this.addDrawLineFn('bezier2', lineBezier2); this.addDrawLineFn('bezier2', lineBezier2);
this.addDrawLineFn('bezier3', lineBezier3); this.addDrawLineFn('bezier3', lineBezier3);
@ -774,4 +843,49 @@ function drawArea(ctx: CanvasRenderingContext2D, pen: MapPen): void {
ctx.fillText(label, x + w / 2, y - fontSize * lineHeight); ctx.fillText(label, x + w / 2, y - fontSize * lineHeight);
ctx.restore(); ctx.restore();
} }
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 } = pen.robot ?? {};
ctx.save();
if (active) {
const ox = x + w / 2;
const oy = y + h / 2;
console.log(ox, oy);
ctx.ellipse(ox, oy, w / 2, h / 2, 0, 0, Math.PI * 2);
ctx.fillStyle = get(theme, 'robot.fill') ?? '';
ctx.fill();
ctx.strokeStyle = get(theme, 'robot.stroke') ?? '';
ctx.stroke();
if (path?.length) {
ctx.strokeStyle = 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);
// todo 缩放时坐标偏移
path.forEach(([ex, ey]) => ctx.lineTo((ex - ox) * s, (ey - oy) * s));
ctx.stroke();
const [x1 = 0, y1 = 0] = nth(path, -1) ?? [];
const [x2 = ox, y2 = oy] = nth(path, -2) ?? [];
const r = Math.atan2(y1 - y2, x1 - x2) + Math.PI;
ctx.setLineDash([0]);
ctx.translate((x1 - ox) * s, (y1 - oy) * 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

View File

@ -5,15 +5,16 @@ import type { Locale as AntdLocale } from 'ant-design-vue/es/locale';
import enUS from 'ant-design-vue/es/locale/en_US'; import enUS from 'ant-design-vue/es/locale/en_US';
import zhCN from 'ant-design-vue/es/locale/zh_CN'; import zhCN from 'ant-design-vue/es/locale/zh_CN';
import dayjs from 'dayjs'; import dayjs from 'dayjs';
import { chain } from 'lodash-es'; import { invert, mapKeys, mapValues } from 'lodash-es';
import { ref, watch } from 'vue'; import { ref, watch } from 'vue';
import { createI18n } from 'vue-i18n'; import { createI18n } from 'vue-i18n';
const LOCALE_FILES = import.meta.glob('asset/locales/*.json', { eager: true, import: 'default' }); const LOCAL_MAP = (() => {
const LOCAL_MAP = chain(LOCALE_FILES) let temp = import.meta.glob('asset/locales/*.json', { eager: true, import: 'default' });
.mapKeys((_, k) => k.match(/^.*[\\|\\/](.+?)\.[^\\.]+$/)?.[1]) temp = mapKeys(temp, (_, k) => k.match(/^.*[\\|\\/](.+?)\.[^\\.]+$/)?.[1]);
.mapValues((v) => <Record<string, string>>v) temp = mapValues(temp, (v) => v);
.value(); return <Record<string, Record<string, string>>>temp;
})();
enum Locale { enum Locale {
= 'zh-CN', = 'zh-CN',
@ -26,10 +27,7 @@ export const i18n = createI18n({
missingWarn: false, missingWarn: false,
fallbackWarn: false, fallbackWarn: false,
locale: Locale.简体中文, locale: Locale.简体中文,
messages: chain(Locale) messages: mapValues(invert(Locale), (_, k) => LOCAL_MAP[k] ?? {}),
.invert()
.mapValues((_, k) => LOCAL_MAP[k] ?? {})
.value(),
}); });
const LOCALE_STORAGE_KEY = 'locale'; const LOCALE_STORAGE_KEY = 'locale';

View File

@ -1,12 +1,13 @@
import { theme, type TokenType as AntdTheme } from 'ant-design-vue'; import { theme, type TokenType as AntdTheme } from 'ant-design-vue';
import { chain } from 'lodash-es'; import { mapKeys, mapValues } from 'lodash-es';
import { ref, watch } from 'vue'; import { ref, watch } from 'vue';
const THEME_FILES = import.meta.glob('asset/themes/*.json', { eager: true, import: 'default' }); const THEME_MAP = (() => {
const THEME_MAP = chain(THEME_FILES) let temp = import.meta.glob('asset/themes/*.json', { eager: true, import: 'default' });
.mapKeys((_, k) => k.match(/^.*[\\|\\/](.+?)\.[^\\.]+$/)?.[1]) temp = mapKeys(temp, (_, k) => k.match(/^.*[\\|\\/](.+?)\.[^\\.]+$/)?.[1]);
.mapValues((v) => <Record<string, string>>v) temp = mapValues(temp, (v) => v);
.value(); return <Record<string, object>>temp;
})();
enum Theme { enum Theme {
Light = 'light', Light = 'light',

8
src/services/ws.ts Normal file
View File

@ -0,0 +1,8 @@
function create(path: string): Promise<WebSocket> {
const ws = new WebSocket((import.meta.env.ENV_WEBSOCKET_BASE ?? '') + path);
return new Promise((resolve, reject) => {
ws.onopen = () => resolve(ws);
ws.onerror = (e) => reject(e);
});
}
export default { create };