temp
1455
package-lock.json
generated
BIN
public/robot/1-active-dark.png
Normal file
After Width: | Height: | Size: 1.7 KiB |
BIN
public/robot/1-active-light.png
Normal file
After Width: | Height: | Size: 1.7 KiB |
BIN
public/robot/1-dark.png
Normal file
After Width: | Height: | Size: 1.6 KiB |
BIN
public/robot/1-light.png
Normal file
After Width: | Height: | Size: 1.6 KiB |
BIN
public/robot/2-active-dark.png
Normal file
After Width: | Height: | Size: 1.9 KiB |
BIN
public/robot/2-active-light.png
Normal file
After Width: | Height: | Size: 1.9 KiB |
BIN
public/robot/2-dark.png
Normal file
After Width: | Height: | Size: 1.7 KiB |
BIN
public/robot/2-light.png
Normal file
After Width: | Height: | Size: 1.7 KiB |
BIN
public/robot/3-active-dark.png
Normal file
After Width: | Height: | Size: 1.9 KiB |
BIN
public/robot/3-active-light.png
Normal file
After Width: | Height: | Size: 1.9 KiB |
BIN
public/robot/3-dark.png
Normal file
After Width: | Height: | Size: 1.7 KiB |
BIN
public/robot/3-light.png
Normal file
After Width: | Height: | Size: 1.7 KiB |
@ -62,7 +62,7 @@ export const EDITOR_CONFIG: Options = {
|
||||
disableAnchor: true,
|
||||
disableEmptyLine: true,
|
||||
disableRepeatLine: true,
|
||||
minScale: 0.19,
|
||||
minScale: 0.24,
|
||||
maxScale: 4.01,
|
||||
scaleOff: 0.01,
|
||||
defaultAnchors: [],
|
||||
@ -71,4 +71,7 @@ export const EDITOR_CONFIG: Options = {
|
||||
fontSize: 14,
|
||||
lineHeight: 1.5,
|
||||
fontFamily: 'system-ui',
|
||||
textRotate: false,
|
||||
textAlign: 'center',
|
||||
textBaseline: 'top',
|
||||
};
|
||||
|
@ -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 { MapAreaType, MapPointType, MapRoutePassType, MapRouteType } from './constant';
|
||||
@ -10,6 +10,7 @@ export interface MapPen extends Pen {
|
||||
point?: MapPointInfo; // 点位信息
|
||||
route?: MapRouteInfo; // 线路信息
|
||||
area?: MapAreaInfo; // 区域信息
|
||||
robot?: MapRobotInfo; // 实时机器人信息
|
||||
|
||||
attrs?: Record<string, unknown>; // 额外属性
|
||||
activeAttrs?: Array<string>; // 已激活的额外属性
|
||||
@ -45,6 +46,10 @@ export interface MapAreaInfo {
|
||||
}
|
||||
//#endregion
|
||||
|
||||
//#region 机器人
|
||||
export type MapRobotInfo = Pick<RobotRealtimeInfo, 'type' | 'active' | 'path'>;
|
||||
//#endregion
|
||||
|
||||
export type Point = Record<'x' | 'y', number>;
|
||||
export type Rect = Record<'x' | 'y' | 'width' | 'height', number>;
|
||||
export type AnchorPosition = 't' | 'b' | 'l' | 'r';
|
||||
|
@ -30,3 +30,11 @@ export interface RobotDetail extends RobotInfo {
|
||||
taskBattery?: number; // 任务电量
|
||||
swapBattery?: number; // 交换电量
|
||||
}
|
||||
|
||||
export interface RobotRealtimeInfo extends RobotInfo {
|
||||
x: number; // 坐标x
|
||||
y: number; // 坐标y
|
||||
active?: boolean; // 是否运行
|
||||
angle?: number; // 旋转角度
|
||||
path?: Array<[number, number]>; // 规划路径
|
||||
}
|
||||
|
@ -1,5 +1,6 @@
|
||||
import type { RobotGroup } from '@api/robot';
|
||||
import http from '@core/http';
|
||||
import ws from '@core/ws';
|
||||
|
||||
import type { GroupSceneDetail, SceneDetail, SceneInfo } from './type';
|
||||
|
||||
@ -10,6 +11,8 @@ const enum API {
|
||||
|
||||
获取组场景 = '/scene/getByGroupId',
|
||||
保存组场景 = '/scene/saveByGroupId',
|
||||
|
||||
实时监控场景 = '/scene/monitor/:id',
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
@ -27,5 +27,10 @@
|
||||
"stroke-12": "#0DBB8A99",
|
||||
"fill-12": "#0DBB8A33",
|
||||
"strokeActive": "#FCC947"
|
||||
},
|
||||
"robot": {
|
||||
"stroke": "#01FDAF99",
|
||||
"fill": "#01FAAD33",
|
||||
"line": "#01fdaf"
|
||||
}
|
||||
}
|
||||
|
@ -27,5 +27,10 @@
|
||||
"stroke-12": "#0DBB8A99",
|
||||
"fill-12": "#0DBB8A33",
|
||||
"strokeActive": "#EBB214"
|
||||
},
|
||||
"robot": {
|
||||
"stroke": "#01FDAF99",
|
||||
"fill": "#01FAAD33",
|
||||
"line": "#01fdaf"
|
||||
}
|
||||
}
|
||||
|
@ -46,6 +46,7 @@ const canDelete = computed<boolean>(() => editor.value.current.value?.name === '
|
||||
class="icon-btn tool-btn"
|
||||
:class="{ active: mode === MapAreaType.库区 }"
|
||||
size="large"
|
||||
:title="$t('添加库区')"
|
||||
@click="mode = MapAreaType.库区"
|
||||
>
|
||||
<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="{ active: mode === MapAreaType.互斥区 }"
|
||||
size="large"
|
||||
:title="$t('添加互斥区')"
|
||||
@click="mode = MapAreaType.互斥区"
|
||||
>
|
||||
<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="{ active: mode === MapAreaType.非互斥区 }"
|
||||
size="large"
|
||||
:title="$t('添加非互斥区')"
|
||||
@click="mode = MapAreaType.非互斥区"
|
||||
>
|
||||
<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-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" />
|
||||
</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" />
|
||||
</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" />
|
||||
</a-button>
|
||||
<a-button
|
||||
class="icon-btn tool-btn ml-12"
|
||||
size="large"
|
||||
:title="$t('删除区域')"
|
||||
@click="editor.deleteById(editor.current.value?.id)"
|
||||
:disabled="!canDelete"
|
||||
>
|
||||
|
@ -14,6 +14,7 @@ import { useRouter } from 'vue-router';
|
||||
type Props = {
|
||||
token: InjectionKey<ShallowRef<EditorService>>;
|
||||
sid: string;
|
||||
showGroupEdit?: boolean;
|
||||
editable?: boolean;
|
||||
current?: string;
|
||||
};
|
||||
@ -149,8 +150,8 @@ const toRemoveRobots = () =>
|
||||
<a-space align="center">
|
||||
<a-button
|
||||
class="icon-btn panel-btn"
|
||||
:title="$t('抢占控制权')"
|
||||
size="small"
|
||||
:title="$t('抢占控制权')"
|
||||
@click="seizeRobots"
|
||||
:disabled="!selected.size"
|
||||
>
|
||||
@ -158,8 +159,8 @@ const toRemoveRobots = () =>
|
||||
</a-button>
|
||||
<a-button
|
||||
class="icon-btn panel-btn"
|
||||
:title="$t('移除机器人')"
|
||||
size="small"
|
||||
:title="$t('移除机器人')"
|
||||
@click="toRemoveRobots"
|
||||
:disabled="!selected.size"
|
||||
>
|
||||
@ -221,7 +222,13 @@ const toRemoveRobots = () =>
|
||||
/>
|
||||
<span>{{ label }}</span>
|
||||
</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" />
|
||||
</a-button>
|
||||
</a-flex>
|
||||
@ -237,8 +244,8 @@ const toRemoveRobots = () =>
|
||||
<template v-if="editable" #actions>
|
||||
<a-button
|
||||
class="icon-btn panel-btn"
|
||||
:title="$t('移除机器人')"
|
||||
size="small"
|
||||
:title="$t('移除机器人')"
|
||||
@click.stop="toRemoveRobot(item.id)"
|
||||
>
|
||||
<i class="icon trash_fill" />
|
||||
|
@ -1,7 +1,9 @@
|
||||
<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 { 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');
|
||||
|
||||
@ -17,14 +19,26 @@ const readScene = async () => {
|
||||
title.value = res?.label ?? '';
|
||||
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
|
||||
|
||||
const title = ref<string>('');
|
||||
watch(
|
||||
() => props.id,
|
||||
() => readScene(),
|
||||
{ immediate: true, flush: 'post' },
|
||||
);
|
||||
|
||||
const container = shallowRef<HTMLDivElement>();
|
||||
const editor = shallowRef<EditorService>();
|
||||
@ -33,6 +47,34 @@ onMounted(() => {
|
||||
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 current = ref<{ type: 'robot' | 'point' | 'line' | 'area'; id: string }>();
|
||||
watch(
|
||||
@ -85,6 +127,18 @@ const selectRobot = (id: string) => {
|
||||
</a-layout-content>
|
||||
</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>
|
||||
|
||||
<style scoped lang="scss">
|
||||
|
@ -130,6 +130,7 @@ const selectRobot = (id: string) => {
|
||||
:editable="editable"
|
||||
:current="current?.id"
|
||||
@change="selectRobot"
|
||||
show-group-edit
|
||||
/>
|
||||
</a-tab-pane>
|
||||
<a-tab-pane key="2" :tab="$t('库区')">
|
||||
|
@ -10,7 +10,7 @@ import {
|
||||
MapRouteType,
|
||||
type Point,
|
||||
} from '@api/map';
|
||||
import type { RobotGroup, RobotInfo } from '@api/robot';
|
||||
import type { RobotGroup, RobotInfo, RobotRealtimeInfo, RobotType } from '@api/robot';
|
||||
import type {
|
||||
GroupSceneDetail,
|
||||
SceneData,
|
||||
@ -22,7 +22,7 @@ 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, 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 { reactive, watch } from 'vue';
|
||||
|
||||
@ -213,16 +213,26 @@ export class EditorService extends Meta2d {
|
||||
),
|
||||
);
|
||||
|
||||
//#region 机器人
|
||||
//#region 机器人组
|
||||
readonly #robotMap = reactive<Map<RobotInfo['id'], RobotInfo>>(new Map());
|
||||
public get robots(): RobotInfo[] {
|
||||
return Array.from(this.#robotMap.values());
|
||||
}
|
||||
|
||||
public checkRobotById(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 });
|
||||
(<SceneData>this.store.data).robots = [...this.#robotMap.values()];
|
||||
}
|
||||
|
||||
public addRobots(gid: RobotInfo['gid'], robots: RobotInfo[]): void {
|
||||
const groups = clone(this.#robotGroups$$.value);
|
||||
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).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) => {
|
||||
const robot = this.#robotMap.get(v);
|
||||
if (isNil(robot)) return;
|
||||
this.#robotMap.set(v, { ...robot, ...values });
|
||||
this.#robotMap.set(v, { ...robot, ...value });
|
||||
});
|
||||
(<SceneData>this.store.data).robots = [...this.#robotMap.values()];
|
||||
}
|
||||
@ -328,8 +338,6 @@ export class EditorService extends Meta2d {
|
||||
|
||||
public deleteById(id?: string): void {
|
||||
const pen = this.getPenById(id);
|
||||
console.log(pen);
|
||||
|
||||
if (pen?.name !== 'area') return;
|
||||
this.delete([pen], true, true);
|
||||
}
|
||||
@ -338,6 +346,61 @@ export class EditorService extends Meta2d {
|
||||
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 点位
|
||||
public readonly points = useObservable<MapPen[], MapPen[]>(
|
||||
this.#change$$.pipe(
|
||||
@ -348,7 +411,7 @@ export class EditorService extends Meta2d {
|
||||
{ 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();
|
||||
const pen: MapPen = {
|
||||
...p,
|
||||
@ -360,7 +423,7 @@ export class EditorService extends Meta2d {
|
||||
label: `P${id}`,
|
||||
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 {
|
||||
@ -526,11 +589,17 @@ export class EditorService extends Meta2d {
|
||||
|
||||
#load(theme: string): void {
|
||||
this.setTheme(theme);
|
||||
|
||||
this.setOptions({ color: get(sTheme.editor, 'color') });
|
||||
this.find('point').forEach((pen) => {
|
||||
if (!pen.point?.type) return;
|
||||
if (pen.point.type < 10) return;
|
||||
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();
|
||||
}
|
||||
|
||||
@ -587,7 +656,7 @@ export class EditorService extends Meta2d {
|
||||
|
||||
#register() {
|
||||
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.addDrawLineFn('bezier2', lineBezier2);
|
||||
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.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
|
||||
|
@ -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 zhCN from 'ant-design-vue/es/locale/zh_CN';
|
||||
import dayjs from 'dayjs';
|
||||
import { chain } from 'lodash-es';
|
||||
import { invert, mapKeys, mapValues } from 'lodash-es';
|
||||
import { ref, watch } from 'vue';
|
||||
import { createI18n } from 'vue-i18n';
|
||||
|
||||
const LOCALE_FILES = import.meta.glob('asset/locales/*.json', { eager: true, import: 'default' });
|
||||
const LOCAL_MAP = chain(LOCALE_FILES)
|
||||
.mapKeys((_, k) => k.match(/^.*[\\|\\/](.+?)\.[^\\.]+$/)?.[1])
|
||||
.mapValues((v) => <Record<string, string>>v)
|
||||
.value();
|
||||
const LOCAL_MAP = (() => {
|
||||
let temp = import.meta.glob('asset/locales/*.json', { eager: true, import: 'default' });
|
||||
temp = mapKeys(temp, (_, k) => k.match(/^.*[\\|\\/](.+?)\.[^\\.]+$/)?.[1]);
|
||||
temp = mapValues(temp, (v) => v);
|
||||
return <Record<string, Record<string, string>>>temp;
|
||||
})();
|
||||
|
||||
enum Locale {
|
||||
简体中文 = 'zh-CN',
|
||||
@ -26,10 +27,7 @@ export const i18n = createI18n({
|
||||
missingWarn: false,
|
||||
fallbackWarn: false,
|
||||
locale: Locale.简体中文,
|
||||
messages: chain(Locale)
|
||||
.invert()
|
||||
.mapValues((_, k) => LOCAL_MAP[k] ?? {})
|
||||
.value(),
|
||||
messages: mapValues(invert(Locale), (_, k) => LOCAL_MAP[k] ?? {}),
|
||||
});
|
||||
|
||||
const LOCALE_STORAGE_KEY = 'locale';
|
||||
|
@ -1,12 +1,13 @@
|
||||
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';
|
||||
|
||||
const THEME_FILES = import.meta.glob('asset/themes/*.json', { eager: true, import: 'default' });
|
||||
const THEME_MAP = chain(THEME_FILES)
|
||||
.mapKeys((_, k) => k.match(/^.*[\\|\\/](.+?)\.[^\\.]+$/)?.[1])
|
||||
.mapValues((v) => <Record<string, string>>v)
|
||||
.value();
|
||||
const THEME_MAP = (() => {
|
||||
let temp = import.meta.glob('asset/themes/*.json', { eager: true, import: 'default' });
|
||||
temp = mapKeys(temp, (_, k) => k.match(/^.*[\\|\\/](.+?)\.[^\\.]+$/)?.[1]);
|
||||
temp = mapValues(temp, (v) => v);
|
||||
return <Record<string, object>>temp;
|
||||
})();
|
||||
|
||||
enum Theme {
|
||||
Light = 'light',
|
||||
|
8
src/services/ws.ts
Normal 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 };
|