feat: scene -> standard

This commit is contained in:
chndfang 2025-05-17 13:08:29 +08:00
parent 6319afa218
commit 7da2ac2f2a
6 changed files with 295 additions and 94 deletions

46
scene.md Normal file
View File

@ -0,0 +1,46 @@
# 标准地图数据结构
```typescript
interface StandardScene {
robotGroups?: Array<RobotGroup>; // 机器人组信息
robots?: Array<RobotInfo>; // 机器人信息
points?: Array<StandardScenePoint>; // 标准点位信息
routes?: Array<StandardSceneRoute>; // 标准线路信息
areas?: Array<StandardSceneArea>; // 标准区域信息
blocks?: Array<[number, number]>; // 障碍点集合
}
interface StandardScenePoint {
id: string;
name: string;
x: number;
y: number;
type: number; // 点位类型
robots?: Array<string>; // 绑定机器人id集合
actions?: Array<string>; // 绑定动作点id集合
config?: object; // 其它属性配置(可按需增加)
properties?: unknown; // 附加数据(前端不做任何处理)
}
interface StandardSceneRoute {
id: string;
connect: [string, string]; // 连接点位id
type: 'line' | 'bezier2' | 'bezier3'; // 线路类型
pass?: number; // 可通行类型
c1?: { x?: number; y?: number }; // 控制点1
c2?: { x?: number; y?: number }; // 控制点2
config?: object; // 其它属性配置(可按需增加)
properties?: unknown; // 附加数据(前端不做任何处理)
}
interface StandardSceneArea {
id: string;
name: string;
x: number;
y: number;
w: number;
h: number;
type: number; // 区域类型
points?: Array<string>; // 绑定点位id集合
routes?: Array<string>; // 绑定线路id集合
config?: object; // 其它属性配置(可按需增加)
properties?: unknown; // 附加数据(前端不做任何处理)
}
```

View File

@ -14,8 +14,6 @@ export enum MapPointType {
,
,
,
= 99,
}
export const MAP_POINT_TYPES = Object.freeze(
<[string, MapPointType][]>Object.entries(MapPointType).filter(([, v]) => typeof v === 'number'),
@ -25,6 +23,7 @@ export const MAP_POINT_TYPES = Object.freeze(
//#region 线路
export enum MapRouteType {
线 = 'line',
线 = 'bezier2',
线 = 'bezier3',
}
export const MAP_ROUTE_TYPE = invert(MapRouteType);

View File

@ -13,6 +13,8 @@ export interface MapPen extends Pen {
attrs?: Record<string, unknown>; // 额外属性
activeAttrs?: Array<string>; // 已激活的额外属性
properties?: unknown; // 第三方附加参数
}
//#region 点位
@ -30,8 +32,8 @@ export interface MapRouteInfo {
type: MapRouteType; // 线路类型
direction?: -1 | 1; // 方向
pass?: MapRoutePassType; // 可通行类型
c1?: Point; // 控制点A
c2?: Point; // 控制点B
c1?: Point; // 控制点1
c2?: Point; // 控制点2
}
//#endregion

View File

@ -9,11 +9,58 @@ export interface SceneDetail extends SceneInfo {
json?: string; // 场景JSON
}
export interface GroupSceneDetail extends SceneDetail {
group: RobotGroup;
robots?: Array<RobotInfo>;
group: RobotGroup; // 机器人组信息
robots?: Array<RobotInfo>; // 机器人信息
}
export interface SceneData extends Meta2dData {
robots?: Array<RobotInfo>;
robotGroups?: Array<RobotGroup>;
robotGroups?: Array<RobotGroup>; // 机器人组信息
robots?: Array<RobotInfo>; // 机器人信息
}
export interface StandardScene {
robotGroups?: Array<RobotGroup>; // 机器人组信息
robots?: Array<RobotInfo>; // 机器人信息
points?: Array<StandardScenePoint>; // 标准点位信息
routes?: Array<StandardSceneRoute>; // 标准线路信息
areas?: Array<StandardSceneArea>; // 标准区域信息
blocks?: Array<[number, number]>; // 障碍点集合
}
export interface StandardScenePoint {
id: string;
name: string;
desc?: string; // 描述
x: number;
y: number;
type: number; // 点位类型
robots?: Array<string>; // 绑定机器人id集合
actions?: Array<string>; // 绑定动作点id集合
config?: object; // 其它属性配置(可按需增加)
properties?: unknown; // 附加数据(前端不做任何处理)
}
export interface StandardSceneRoute {
id: string;
desc?: string; // 描述
from: string; // 起点点位id
to: string; // 终点点位id
type: 'line' | 'bezier2' | 'bezier3'; // 线路类型
pass?: number; // 可通行类型
c1?: { x?: number; y?: number }; // 控制点1
c2?: { x?: number; y?: number }; // 控制点2
config?: object; // 其它属性配置(可按需增加)
properties?: unknown; // 附加数据(前端不做任何处理)
}
export interface StandardSceneArea {
id: string;
name: string;
desc?: string; // 描述
x: number;
y: number;
w: number;
h: number;
type: number; // 区域类型
points?: Array<string>; // 绑定点位id集合
routes?: Array<string>; // 绑定线路id集合
config?: object; // 其它属性配置(可按需增加)
properties?: unknown; // 附加数据(前端不做任何处理)
}

View File

@ -92,71 +92,73 @@ const route = computed<MapRouteInfo | null>(() => {
</a-col>
</a-row>
<template v-if="MapRouteType.三阶贝塞尔曲线 === route.type">
<a-row align="middle" :gutter="8">
<a-col flex="auto">
<a-typography-text>{{ $t('控制点1') }}:</a-typography-text>
</a-col>
<a-col flex="none">
<a-space :size="8">
<a-typography-text code>X:</a-typography-text>
<a-input-number
style="width: 80px"
:placeholder="$t('请输入')"
:precision="0"
:controls="false"
:value="route?.c1?.x.toFixed()"
@change="editor.updateRoute(id, { c1: { x: +$event, y: route?.c1?.y ?? 0 } })"
/>
</a-space>
</a-col>
<a-col flex="none">
<a-space :size="8">
<a-typography-text code>Y:</a-typography-text>
<a-input-number
style="width: 80px"
:placeholder="$t('请输入')"
:precision="0"
:controls="false"
:value="route?.c1?.y.toFixed()"
@change="editor.updateRoute(id, { c1: { x: route?.c1?.x ?? 0, y: +$event } })"
/>
</a-space>
</a-col>
</a-row>
<a-row
v-if="[MapRouteType.二阶贝塞尔曲线, MapRouteType.三阶贝塞尔曲线].includes(route.type)"
align="middle"
:gutter="8"
>
<a-col flex="auto">
<a-typography-text>{{ $t('控制点1') }}:</a-typography-text>
</a-col>
<a-col flex="none">
<a-space :size="8">
<a-typography-text code>X:</a-typography-text>
<a-input-number
style="width: 80px"
:placeholder="$t('请输入')"
:precision="0"
:controls="false"
:value="route?.c1?.x.toFixed()"
@change="editor.updateRoute(id, { c1: { x: +$event, y: route?.c1?.y ?? 0 } })"
/>
</a-space>
</a-col>
<a-col flex="none">
<a-space :size="8">
<a-typography-text code>Y:</a-typography-text>
<a-input-number
style="width: 80px"
:placeholder="$t('请输入')"
:precision="0"
:controls="false"
:value="route?.c1?.y.toFixed()"
@change="editor.updateRoute(id, { c1: { x: route?.c1?.x ?? 0, y: +$event } })"
/>
</a-space>
</a-col>
</a-row>
<a-row align="middle" :gutter="8">
<a-col flex="auto">
<a-typography-text>{{ $t('控制点2') }}:</a-typography-text>
</a-col>
<a-col flex="none">
<a-space :size="8">
<a-typography-text code>X:</a-typography-text>
<a-input-number
style="width: 80px"
:placeholder="$t('请输入')"
:precision="0"
:controls="false"
:value="route?.c2?.x.toFixed()"
@change="editor.updateRoute(id, { c2: { x: +$event, y: route?.c2?.y ?? 0 } })"
/>
</a-space>
</a-col>
<a-col flex="none">
<a-space :size="8">
<a-typography-text code>Y:</a-typography-text>
<a-input-number
style="width: 80px"
:placeholder="$t('请输入')"
:precision="0"
:controls="false"
:value="route?.c2?.y.toFixed()"
@change="editor.updateRoute(id, { c2: { x: route?.c2?.x ?? 0, y: +$event } })"
/>
</a-space>
</a-col>
</a-row>
</template>
<a-row v-if="MapRouteType.三阶贝塞尔曲线 === route.type" align="middle" :gutter="8">
<a-col flex="auto">
<a-typography-text>{{ $t('控制点2') }}:</a-typography-text>
</a-col>
<a-col flex="none">
<a-space :size="8">
<a-typography-text code>X:</a-typography-text>
<a-input-number
style="width: 80px"
:placeholder="$t('请输入')"
:precision="0"
:controls="false"
:value="route?.c2?.x.toFixed()"
@change="editor.updateRoute(id, { c2: { x: +$event, y: route?.c2?.y ?? 0 } })"
/>
</a-space>
</a-col>
<a-col flex="none">
<a-space :size="8">
<a-typography-text code>Y:</a-typography-text>
<a-input-number
style="width: 80px"
:placeholder="$t('请输入')"
:precision="0"
:controls="false"
:value="route?.c2?.y.toFixed()"
@change="editor.updateRoute(id, { c2: { x: route?.c2?.x ?? 0, y: +$event } })"
/>
</a-space>
</a-col>
</a-row>
</a-flex>
<a-empty v-else :image="sTheme.empty" />
</a-card>

View File

@ -12,15 +12,23 @@ import {
type Point,
} from '@api/map';
import type { RobotGroup, RobotInfo } from '@api/robot';
import type { GroupSceneDetail, SceneData } from '@api/scene';
import type {
GroupSceneDetail,
SceneData,
StandardScene,
StandardSceneArea,
StandardScenePoint,
StandardSceneRoute,
} from '@api/scene';
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, isNil, isString, pick, remove, some } from 'lodash-es';
import { clone, get, isEmpty, isNil, isString, pick, remove, some } from 'lodash-es';
import { BehaviorSubject, debounceTime, filter, map, Subject, switchMap } from 'rxjs';
import { reactive, watch } from 'vue';
export class EditorService extends Meta2d {
//#region 场景文件
public async load(map?: string, editable = false, detail?: Partial<GroupSceneDetail>): Promise<void> {
let data = map ? JSON.parse(map) : undefined;
if (!isNil(detail)) {
@ -32,22 +40,101 @@ export class EditorService extends Meta2d {
this.setState(editable);
}
public save(): string {
const data = this.data();
data.pens.forEach((pen: MapPen) => {
remove(pen.point?.robots ?? [], (v) => !this.#robotMap.has(v));
remove(pen.point?.actions ?? [], (v) => this.getPenById(v)?.point?.type !== MapPointType.);
remove(pen.area?.points ?? [], (v) => {
const { point } = this.getPenById(v) ?? {};
if (isNil(point)) return true;
if (point.type === MapPointType.) return true;
if (pen.area?.type === MapAreaType. && point.type !== MapPointType.) return true;
return false;
});
remove(pen.area?.routes ?? [], (v) => isNil(this.getPenById(v)));
});
return JSON.stringify(data);
const scene: StandardScene = {
robotGroups: this.robotGroups.value,
robots: this.robots,
points: this.points.value.map((v) => this.#mapScenePoint(v)).filter((v) => !isNil(v)),
routes: this.routes.value.map((v) => this.#mapSceneRoute(v)).filter((v) => !isNil(v)),
areas: this.areas.value.map((v) => this.#mapSceneArea(v)).filter((v) => !isNil(v)),
blocks: [],
};
return JSON.stringify(scene);
}
#mapScenePoint(pen?: MapPen): StandardScenePoint | null {
if (!pen?.id || isEmpty(pen?.point)) return null;
const { id, x = 0, y = 0, label, desc, properties } = pen;
const { type, robots, actions } = pen.point;
const point: StandardScenePoint = {
id: id,
name: label || id,
desc,
x,
y,
type,
config: {},
properties,
};
if ([MapPointType., MapPointType.].includes(type)) {
point.robots = robots?.filter((v) => this.#robotMap.has(v));
}
if (MapPointType. === type) {
point.actions = actions?.filter((v) => this.getPenById(v)?.point?.type === MapPointType.);
}
return point;
}
#mapSceneRoute(pen?: MapPen): StandardSceneRoute | null {
if (!pen?.id || pen.anchors?.length !== 2 || isEmpty(pen?.route)) return null;
const { id, anchors, desc, properties } = pen;
const { type, direction = 1, pass, c1, c2 } = pen.route;
const [p1, p2] = anchors.map((v) => v.connectTo!);
const route: StandardSceneRoute = {
id: id,
desc,
from: direction < 0 ? p2 : p1,
to: direction < 0 ? p1 : p2,
type,
pass,
config: {},
properties,
};
switch (type) {
case MapRouteType.线:
route.c1 = c1;
break;
case MapRouteType.线:
route.c1 = direction < 0 ? c2 : c1;
route.c2 = direction < 0 ? c1 : c2;
break;
default:
break;
}
return route;
}
#mapSceneArea(pen: MapPen): StandardSceneArea | null {
if (!pen.id || isEmpty(pen.area)) return null;
const { id, x = 0, y = 0, width = 0, height = 0, label, desc, properties } = pen;
const { type, points, routes } = pen.area;
const area: StandardSceneArea = {
id,
name: label || id,
desc,
x,
y,
w: width,
h: height,
type,
config: {},
properties,
};
if (MapAreaType. === type) {
area.points = points?.filter((v) => this.getPenById(v)?.point?.type === MapPointType.);
}
if ([MapAreaType., MapAreaType.].includes(type)) {
area.points = points?.filter((v) => {
const { point } = this.getPenById(v) ?? {};
if (isNil(point)) return false;
if (point.type === MapPointType.) return false;
return true;
});
}
if (MapAreaType. === type) {
area.routes = routes?.filter((v) => !isEmpty(this.getPenById(v)?.area));
}
return area;
}
//#endregion
public setState(editable?: boolean): void {
this.lock(editable ? LockState.None : LockState.DisableEdit);
}
@ -458,6 +545,7 @@ export class EditorService extends Meta2d {
this.register({ line: () => new Path2D() });
this.registerCanvasDraw({ point: drawPoint, line: drawLine, area: drawArea });
this.registerAnchors({ point: anchorPoint });
this.addDrawLineFn('bezier2', lineBezier2);
this.addDrawLineFn('bezier3', lineBezier3);
}
}
@ -545,7 +633,7 @@ function drawLine(ctx: CanvasRenderingContext2D, pen: MapPen): void {
const { x: x2 = 0, y: y2 = 0 } = p2 ?? {};
const { type, direction = 1, pass = 0, c1, c2 } = pen.route ?? {};
const { x: dx1 = x2 - x1, y: dy1 = 0 } = c1 ?? {};
const { x: dx2 = 0, y: dy2 = y2 - y1 } = c2 ?? {};
const { x: dx2 = 0, y: dy2 = y1 - y2 } = c2 ?? {};
ctx.save();
ctx.beginPath();
@ -555,10 +643,15 @@ function drawLine(ctx: CanvasRenderingContext2D, pen: MapPen): void {
case MapRouteType.线:
ctx.lineTo(x2, y2);
break;
case MapRouteType.线:
ctx.quadraticCurveTo(x1 + dx1, y1 + dy1, x2, y2);
p1.next = { x: x1 + (2 / 3) * dx1, y: y1 + (2 / 3) * dy1 };
p2.prev = { x: x2 / 3 + (2 / 3) * (x1 + dx1), y: y2 / 3 + (2 / 3) * (y1 + dy1) };
break;
case MapRouteType.线:
ctx.bezierCurveTo(x1 + dx1, y1 + dy1, x2 - dx2, y2 - dy2, x2, y2);
ctx.bezierCurveTo(x1 + dx1, y1 + dy1, x2 + dx2, y2 + dy2, x2, y2);
p1.next = { x: x1 + dx1, y: y1 + dy1 };
p2.prev = { x: x2 - dx2, y: y2 - dy2 };
p2.prev = { x: x2 + dx2, y: y2 + dy2 };
break;
default:
break;
@ -573,8 +666,10 @@ function drawLine(ctx: CanvasRenderingContext2D, pen: MapPen): void {
switch (type) {
case MapRouteType.线:
return Math.atan2(y2 - y1, x2 - x1);
case MapRouteType.线:
return direction < 0 ? Math.atan2(dy1, dx1) : Math.atan2(y2 - y1 - dy1, x2 - x1 - dx1);
case MapRouteType.线:
return direction < 0 ? Math.atan2(dy1, dx1) : Math.atan2(dy2, dx2);
return direction < 0 ? Math.atan2(dy1, dx1) : Math.atan2(-dy2, -dx2);
default:
return 0;
}
@ -592,6 +687,16 @@ function drawLine(ctx: CanvasRenderingContext2D, pen: MapPen): void {
ctx.setTransform(1, 0, 0, 1, 0, 0);
ctx.restore();
}
function lineBezier2(_: Meta2dStore, pen: MapPen): void {
if (pen.calculative?.worldAnchors?.length !== 2) return;
const { c1 } = pen.route ?? {};
const [p1, p2] = pen.calculative?.worldAnchors ?? [];
const { x: x1 = 0, y: y1 = 0 } = p1 ?? {};
const { x: x2 = 0, y: y2 = 0 } = p2 ?? {};
const { x: dx = x2 - x1, y: dy = 0 } = c1 ?? {};
pen.calculative.worldAnchors[0].next = { x: x1 + (2 / 3) * dx, y: y1 + (2 / 3) * dy };
pen.calculative.worldAnchors[1].prev = { x: x2 / 3 + (2 / 3) * (x1 + dx), y: y2 / 3 + (2 / 3) * (y1 + dy) };
}
function lineBezier3(_: Meta2dStore, pen: MapPen): void {
if (pen.calculative?.worldAnchors?.length !== 2) return;
const { c1, c2 } = pen.route ?? {};
@ -599,9 +704,9 @@ function lineBezier3(_: Meta2dStore, pen: MapPen): void {
const { x: x1 = 0, y: y1 = 0 } = p1 ?? {};
const { x: x2 = 0, y: y2 = 0 } = p2 ?? {};
const { x: dx1 = x2 - x1, y: dy1 = 0 } = c1 ?? {};
const { x: dx2 = 0, y: dy2 = y2 - y1 } = c2 ?? {};
const { x: dx2 = 0, y: dy2 = y1 - y2 } = c2 ?? {};
pen.calculative.worldAnchors[0].next = { x: x1 + dx1, y: y1 + dy1 };
pen.calculative.worldAnchors[1].prev = { x: x2 - dx2, y: y2 - dy2 };
pen.calculative.worldAnchors[1].prev = { x: x2 + dx2, y: y2 + dy2 };
}
function drawArea(ctx: CanvasRenderingContext2D, pen: MapPen): void {