diff --git a/scene.md b/scene.md new file mode 100644 index 0000000..e5c4901 --- /dev/null +++ b/scene.md @@ -0,0 +1,46 @@ +# 标准地图数据结构 + +```typescript +interface StandardScene { + robotGroups?: Array; // 机器人组信息 + robots?: Array; // 机器人信息 + points?: Array; // 标准点位信息 + routes?: Array; // 标准线路信息 + areas?: Array; // 标准区域信息 + blocks?: Array<[number, number]>; // 障碍点集合 +} +interface StandardScenePoint { + id: string; + name: string; + x: number; + y: number; + type: number; // 点位类型 + robots?: Array; // 绑定机器人id集合 + actions?: Array; // 绑定动作点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; // 绑定点位id集合 + routes?: Array; // 绑定线路id集合 + config?: object; // 其它属性配置(可按需增加) + properties?: unknown; // 附加数据(前端不做任何处理) +} +``` diff --git a/src/apis/map/constant.ts b/src/apis/map/constant.ts index f1f800a..9912265 100644 --- a/src/apis/map/constant.ts +++ b/src/apis/map/constant.ts @@ -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); diff --git a/src/apis/map/type.ts b/src/apis/map/type.ts index 0c9abf1..1a340e8 100644 --- a/src/apis/map/type.ts +++ b/src/apis/map/type.ts @@ -13,6 +13,8 @@ export interface MapPen extends Pen { attrs?: Record; // 额外属性 activeAttrs?: Array; // 已激活的额外属性 + + 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 diff --git a/src/apis/scene/type.ts b/src/apis/scene/type.ts index 3de82eb..b4bc47d 100644 --- a/src/apis/scene/type.ts +++ b/src/apis/scene/type.ts @@ -9,11 +9,58 @@ export interface SceneDetail extends SceneInfo { json?: string; // 场景JSON } export interface GroupSceneDetail extends SceneDetail { - group: RobotGroup; - robots?: Array; + group: RobotGroup; // 机器人组信息 + robots?: Array; // 机器人信息 } export interface SceneData extends Meta2dData { - robots?: Array; - robotGroups?: Array; + robotGroups?: Array; // 机器人组信息 + robots?: Array; // 机器人信息 +} + +export interface StandardScene { + robotGroups?: Array; // 机器人组信息 + robots?: Array; // 机器人信息 + points?: Array; // 标准点位信息 + routes?: Array; // 标准线路信息 + areas?: Array; // 标准区域信息 + blocks?: Array<[number, number]>; // 障碍点集合 +} +export interface StandardScenePoint { + id: string; + name: string; + desc?: string; // 描述 + x: number; + y: number; + type: number; // 点位类型 + robots?: Array; // 绑定机器人id集合 + actions?: Array; // 绑定动作点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; // 绑定点位id集合 + routes?: Array; // 绑定线路id集合 + config?: object; // 其它属性配置(可按需增加) + properties?: unknown; // 附加数据(前端不做任何处理) } diff --git a/src/components/card/route-edit-card.vue b/src/components/card/route-edit-card.vue index f6393f0..b141458 100644 --- a/src/components/card/route-edit-card.vue +++ b/src/components/card/route-edit-card.vue @@ -92,71 +92,73 @@ const route = computed(() => { - + + + {{ $t('控制点2') }}: + + + + X: + + + + + + Y: + + + + diff --git a/src/services/editor.service.ts b/src/services/editor.service.ts index 7dea483..153b095 100644 --- a/src/services/editor.service.ts +++ b/src/services/editor.service.ts @@ -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): Promise { 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 {