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

View File

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

View File

@ -9,11 +9,58 @@ export interface SceneDetail extends SceneInfo {
json?: string; // 场景JSON json?: string; // 场景JSON
} }
export interface GroupSceneDetail extends SceneDetail { export interface GroupSceneDetail extends SceneDetail {
group: RobotGroup; group: RobotGroup; // 机器人组信息
robots?: Array<RobotInfo>; robots?: Array<RobotInfo>; // 机器人信息
} }
export interface SceneData extends Meta2dData { 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-col>
</a-row> </a-row>
<template v-if="MapRouteType.三阶贝塞尔曲线 === route.type"> <a-row
<a-row align="middle" :gutter="8"> v-if="[MapRouteType.二阶贝塞尔曲线, MapRouteType.三阶贝塞尔曲线].includes(route.type)"
<a-col flex="auto"> align="middle"
<a-typography-text>{{ $t('控制点1') }}:</a-typography-text> :gutter="8"
</a-col> >
<a-col flex="none"> <a-col flex="auto">
<a-space :size="8"> <a-typography-text>{{ $t('控制点1') }}:</a-typography-text>
<a-typography-text code>X:</a-typography-text> </a-col>
<a-input-number <a-col flex="none">
style="width: 80px" <a-space :size="8">
:placeholder="$t('请输入')" <a-typography-text code>X:</a-typography-text>
:precision="0" <a-input-number
:controls="false" style="width: 80px"
:value="route?.c1?.x.toFixed()" :placeholder="$t('请输入')"
@change="editor.updateRoute(id, { c1: { x: +$event, y: route?.c1?.y ?? 0 } })" :precision="0"
/> :controls="false"
</a-space> :value="route?.c1?.x.toFixed()"
</a-col> @change="editor.updateRoute(id, { c1: { x: +$event, y: route?.c1?.y ?? 0 } })"
<a-col flex="none"> />
<a-space :size="8"> </a-space>
<a-typography-text code>Y:</a-typography-text> </a-col>
<a-input-number <a-col flex="none">
style="width: 80px" <a-space :size="8">
:placeholder="$t('请输入')" <a-typography-text code>Y:</a-typography-text>
:precision="0" <a-input-number
:controls="false" style="width: 80px"
:value="route?.c1?.y.toFixed()" :placeholder="$t('请输入')"
@change="editor.updateRoute(id, { c1: { x: route?.c1?.x ?? 0, y: +$event } })" :precision="0"
/> :controls="false"
</a-space> :value="route?.c1?.y.toFixed()"
</a-col> @change="editor.updateRoute(id, { c1: { x: route?.c1?.x ?? 0, y: +$event } })"
</a-row> />
</a-space>
</a-col>
</a-row>
<a-row align="middle" :gutter="8"> <a-row v-if="MapRouteType.三阶贝塞尔曲线 === route.type" align="middle" :gutter="8">
<a-col flex="auto"> <a-col flex="auto">
<a-typography-text>{{ $t('控制点2') }}:</a-typography-text> <a-typography-text>{{ $t('控制点2') }}:</a-typography-text>
</a-col> </a-col>
<a-col flex="none"> <a-col flex="none">
<a-space :size="8"> <a-space :size="8">
<a-typography-text code>X:</a-typography-text> <a-typography-text code>X:</a-typography-text>
<a-input-number <a-input-number
style="width: 80px" style="width: 80px"
:placeholder="$t('请输入')" :placeholder="$t('请输入')"
:precision="0" :precision="0"
:controls="false" :controls="false"
:value="route?.c2?.x.toFixed()" :value="route?.c2?.x.toFixed()"
@change="editor.updateRoute(id, { c2: { x: +$event, y: route?.c2?.y ?? 0 } })" @change="editor.updateRoute(id, { c2: { x: +$event, y: route?.c2?.y ?? 0 } })"
/> />
</a-space> </a-space>
</a-col> </a-col>
<a-col flex="none"> <a-col flex="none">
<a-space :size="8"> <a-space :size="8">
<a-typography-text code>Y:</a-typography-text> <a-typography-text code>Y:</a-typography-text>
<a-input-number <a-input-number
style="width: 80px" style="width: 80px"
:placeholder="$t('请输入')" :placeholder="$t('请输入')"
:precision="0" :precision="0"
:controls="false" :controls="false"
:value="route?.c2?.y.toFixed()" :value="route?.c2?.y.toFixed()"
@change="editor.updateRoute(id, { c2: { x: route?.c2?.x ?? 0, y: +$event } })" @change="editor.updateRoute(id, { c2: { x: route?.c2?.x ?? 0, y: +$event } })"
/> />
</a-space> </a-space>
</a-col> </a-col>
</a-row> </a-row>
</template>
</a-flex> </a-flex>
<a-empty v-else :image="sTheme.empty" /> <a-empty v-else :image="sTheme.empty" />
</a-card> </a-card>

View File

@ -12,15 +12,23 @@ import {
type Point, type Point,
} from '@api/map'; } from '@api/map';
import type { RobotGroup, RobotInfo } from '@api/robot'; 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 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, 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 { BehaviorSubject, debounceTime, filter, map, Subject, switchMap } from 'rxjs';
import { reactive, watch } from 'vue'; import { reactive, watch } from 'vue';
export class EditorService extends Meta2d { export class EditorService extends Meta2d {
//#region 场景文件
public async load(map?: string, editable = false, detail?: Partial<GroupSceneDetail>): Promise<void> { public async load(map?: string, editable = false, detail?: Partial<GroupSceneDetail>): Promise<void> {
let data = map ? JSON.parse(map) : undefined; let data = map ? JSON.parse(map) : undefined;
if (!isNil(detail)) { if (!isNil(detail)) {
@ -32,22 +40,101 @@ export class EditorService extends Meta2d {
this.setState(editable); this.setState(editable);
} }
public save(): string { public save(): string {
const data = this.data(); const scene: StandardScene = {
data.pens.forEach((pen: MapPen) => { robotGroups: this.robotGroups.value,
remove(pen.point?.robots ?? [], (v) => !this.#robotMap.has(v)); robots: this.robots,
remove(pen.point?.actions ?? [], (v) => this.getPenById(v)?.point?.type !== MapPointType.); points: this.points.value.map((v) => this.#mapScenePoint(v)).filter((v) => !isNil(v)),
remove(pen.area?.points ?? [], (v) => { routes: this.routes.value.map((v) => this.#mapSceneRoute(v)).filter((v) => !isNil(v)),
const { point } = this.getPenById(v) ?? {}; areas: this.areas.value.map((v) => this.#mapSceneArea(v)).filter((v) => !isNil(v)),
if (isNil(point)) return true; blocks: [],
if (point.type === MapPointType.) return true; };
if (pen.area?.type === MapAreaType. && point.type !== MapPointType.) return true; return JSON.stringify(scene);
return false;
});
remove(pen.area?.routes ?? [], (v) => isNil(this.getPenById(v)));
});
return JSON.stringify(data);
} }
#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 { public setState(editable?: boolean): void {
this.lock(editable ? LockState.None : LockState.DisableEdit); this.lock(editable ? LockState.None : LockState.DisableEdit);
} }
@ -458,6 +545,7 @@ export class EditorService extends Meta2d {
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 });
this.registerAnchors({ point: anchorPoint }); this.registerAnchors({ point: anchorPoint });
this.addDrawLineFn('bezier2', lineBezier2);
this.addDrawLineFn('bezier3', lineBezier3); this.addDrawLineFn('bezier3', lineBezier3);
} }
} }
@ -545,7 +633,7 @@ function drawLine(ctx: CanvasRenderingContext2D, pen: MapPen): void {
const { x: x2 = 0, y: y2 = 0 } = p2 ?? {}; const { x: x2 = 0, y: y2 = 0 } = p2 ?? {};
const { type, direction = 1, pass = 0, c1, c2 } = pen.route ?? {}; const { type, direction = 1, pass = 0, c1, c2 } = pen.route ?? {};
const { x: dx1 = x2 - x1, y: dy1 = 0 } = c1 ?? {}; 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.save();
ctx.beginPath(); ctx.beginPath();
@ -555,10 +643,15 @@ function drawLine(ctx: CanvasRenderingContext2D, pen: MapPen): void {
case MapRouteType.线: case MapRouteType.线:
ctx.lineTo(x2, y2); ctx.lineTo(x2, y2);
break; 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.线: 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 }; 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; break;
default: default:
break; break;
@ -573,8 +666,10 @@ function drawLine(ctx: CanvasRenderingContext2D, pen: MapPen): void {
switch (type) { switch (type) {
case MapRouteType.线: case MapRouteType.线:
return Math.atan2(y2 - y1, x2 - x1); 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.线: 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: default:
return 0; return 0;
} }
@ -592,6 +687,16 @@ function drawLine(ctx: CanvasRenderingContext2D, pen: MapPen): void {
ctx.setTransform(1, 0, 0, 1, 0, 0); ctx.setTransform(1, 0, 0, 1, 0, 0);
ctx.restore(); 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 { function lineBezier3(_: Meta2dStore, pen: MapPen): void {
if (pen.calculative?.worldAnchors?.length !== 2) return; if (pen.calculative?.worldAnchors?.length !== 2) return;
const { c1, c2 } = pen.route ?? {}; 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: x1 = 0, y: y1 = 0 } = p1 ?? {};
const { x: x2 = 0, y: y2 = 0 } = p2 ?? {}; const { x: x2 = 0, y: y2 = 0 } = p2 ?? {};
const { x: dx1 = x2 - x1, y: dy1 = 0 } = c1 ?? {}; 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[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 { function drawArea(ctx: CanvasRenderingContext2D, pen: MapPen): void {