feat: 新增约束区类型及最大可容纳AMR数配置,优化场景编辑功能

This commit is contained in:
xudan 2025-07-01 14:38:21 +08:00
parent 0479943204
commit 33f665d5fd
13 changed files with 81 additions and 11 deletions

View File

@ -102,6 +102,8 @@ export enum MapAreaType {
= 11, = 11,
/** 非互斥区 - 可以同时有多个机器人进入的区域 */ /** 非互斥区 - 可以同时有多个机器人进入的区域 */
, ,
/** 约束区 - 机器人运动受到特定约束的区域 */
,
} }
/** /**

View File

@ -43,6 +43,7 @@ export interface MapAreaInfo {
type: MapAreaType; // 区域类型 type: MapAreaType; // 区域类型
points?: Array<string>; // 绑定点位id集合 points?: Array<string>; // 绑定点位id集合
routes?: Array<string>; // 绑定线路id集合 routes?: Array<string>; // 绑定线路id集合
maxAmr?: number; // 最大可容纳AMR数
} }
//#endregion //#endregion

View File

@ -64,6 +64,7 @@ export interface StandardSceneArea {
type: number; // 区域类型 type: number; // 区域类型
points?: Array<string>; // 绑定点位id集合 points?: Array<string>; // 绑定点位id集合
routes?: Array<string>; // 绑定线路id集合 routes?: Array<string>; // 绑定线路id集合
maxAmr?: number; // 最大可容纳AMR数
config?: object; // 其它属性配置(可按需增加) config?: object; // 其它属性配置(可按需增加)
properties?: unknown; // 附加数据(前端不做任何处理) properties?: unknown; // 附加数据(前端不做任何处理)
} }

View File

@ -8,6 +8,9 @@ $icons: (
area12-active, area12-active,
area12-detail, area12-detail,
area12, area12,
area13-active,
area13-detail,
area13,
battery_charge, battery_charge,
battery, battery,
connect_off, connect_off,

Binary file not shown.

After

Width:  |  Height:  |  Size: 660 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 590 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 578 B

View File

@ -28,6 +28,8 @@
"fill-11": "#FF9A9A33", "fill-11": "#FF9A9A33",
"stroke-12": "#0DBB8A99", "stroke-12": "#0DBB8A99",
"fill-12": "#0DBB8A33", "fill-12": "#0DBB8A33",
"stroke-13": "#e61e4aad",
"fill-13": "#e61e4a33",
"strokeActive": "#FCC947" "strokeActive": "#FCC947"
}, },
"robot": { "robot": {

View File

@ -65,6 +65,21 @@ const routes = computed<string[]>(
</a-input> </a-input>
</a-col> </a-col>
</a-row> </a-row>
<a-row v-if="area.type === MapAreaType.约束区" :gutter="[8, 8]">
<a-col :span="24">
<a-typography-text>{{ $t('最大可容纳AMR数') }}:</a-typography-text>
</a-col>
<a-col :span="24">
<a-input-number
class="full"
:placeholder="$t('请输入数量')"
:min="0"
:max="99"
:value="pen.area?.maxAmr"
@change="editor.updateArea(id, { maxAmr: $event as number })"
/>
</a-col>
</a-row>
<a-row :gutter="[8, 8]"> <a-row :gutter="[8, 8]">
<a-col :span="24"> <a-col :span="24">
@ -108,7 +123,7 @@ const routes = computed<string[]>(
</a-collapse-panel> </a-collapse-panel>
<a-collapse-panel <a-collapse-panel
v-if="[MapAreaType.互斥区, MapAreaType.非互斥区].includes(area.type)" v-if="[MapAreaType.互斥区, MapAreaType.非互斥区, MapAreaType.约束区].includes(area.type)"
:header="$t('绑定站点')" :header="$t('绑定站点')"
> >
<template #extra> <template #extra>

View File

@ -42,7 +42,7 @@ const canDelete = computed<boolean>(() => editor.value.current.value?.name === '
<template> <template>
<a-space class="toolbar" :size="0"> <a-space class="toolbar" :size="0">
<a-button <!-- <a-button
class="icon-btn tool-btn" class="icon-btn tool-btn"
:class="{ active: mode === MapAreaType.库区 }" :class="{ active: mode === MapAreaType.库区 }"
size="large" size="large"
@ -50,6 +50,15 @@ const canDelete = computed<boolean>(() => editor.value.current.value?.name === '
@click="mode = MapAreaType.库区" @click="mode = MapAreaType.库区"
> >
<i class="icon" :class="mode === MapAreaType.库区 ? 'area1-active' : 'area1'" /> <i class="icon" :class="mode === MapAreaType.库区 ? 'area1-active' : 'area1'" />
</a-button> -->
<a-button
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'" />
</a-button> </a-button>
<a-button <a-button
class="icon-btn tool-btn ml-12" class="icon-btn tool-btn ml-12"

View File

@ -2,6 +2,8 @@
import type { RobotRealtimeInfo } from '@api/robot'; import type { RobotRealtimeInfo } from '@api/robot';
import { getSceneByGroupId, getSceneById, monitorSceneById } from '@api/scene'; import { getSceneByGroupId, getSceneById, monitorSceneById } from '@api/scene';
import { EditorService } from '@core/editor.service'; import { EditorService } from '@core/editor.service';
import { useViewState } from '@core/useViewState';
import { message } from 'ant-design-vue';
import { isNil } from 'lodash-es'; import { isNil } from 'lodash-es';
import { computed, onMounted, onUnmounted, provide, ref, shallowRef, watch } from 'vue'; import { computed, onMounted, onUnmounted, provide, ref, shallowRef, watch } from 'vue';
@ -52,6 +54,8 @@ onMounted(async () => {
await readScene(); await readScene();
await editor.value?.initRobots(); await editor.value?.initRobots();
await monitorScene(); await monitorScene();
//
await handleRestoreViewState();
}); });
onUnmounted(() => { onUnmounted(() => {
client.value?.close(); client.value?.close();
@ -80,6 +84,32 @@ const selectRobot = (id: string) => {
current.value = { type: 'robot', id }; current.value = { type: 'robot', id };
editor.value?.inactive(); editor.value?.inactive();
}; };
//
const { saveViewState, restoreViewState, isSaving } = useViewState();
//
const handleSaveViewState = async () => {
if (!editor.value) return;
try {
await saveViewState(editor.value, props.sid, props.id);
message.success('视图状态保存成功');
} catch {
message.error('视图状态保存失败');
}
};
//
const handleRestoreViewState = async () => {
if (!editor.value) return;
const restored = await restoreViewState(editor.value, props.sid, props.id);
if (!restored) {
// 使centerView
editor.value.centerView();
}
};
</script> </script>
<template> <template>
@ -87,6 +117,7 @@ const selectRobot = (id: string) => {
<a-layout-header class="p-16" style="height: 64px"> <a-layout-header class="p-16" style="height: 64px">
<a-flex justify="space-between" align="center"> <a-flex justify="space-between" align="center">
<a-typography-text class="title">{{ title }}--场景仿真</a-typography-text> <a-typography-text class="title">{{ title }}--场景仿真</a-typography-text>
<!-- <a-button type="primary" :loading="isSaving" @click="handleSaveViewState"> 保存比例 </a-button> -->
</a-flex> </a-flex>
</a-layout-header> </a-layout-header>
@ -96,9 +127,9 @@ const selectRobot = (id: string) => {
<a-tab-pane key="1" :tab="$t('机器人')"> <a-tab-pane key="1" :tab="$t('机器人')">
<RobotGroups v-if="editor" :token="EDITOR_KEY" :sid="sid" :current="current?.id" @change="selectRobot" /> <RobotGroups v-if="editor" :token="EDITOR_KEY" :sid="sid" :current="current?.id" @change="selectRobot" />
</a-tab-pane> </a-tab-pane>
<a-tab-pane key="2" :tab="$t('库区')"> <!-- <a-tab-pane key="2" :tab="$t('库区')">
<PenGroups v-if="editor" :token="EDITOR_KEY" :current="current?.id" only-area1 /> <PenGroups v-if="editor" :token="EDITOR_KEY" :current="current?.id" only-area1 />
</a-tab-pane> </a-tab-pane> -->
<a-tab-pane key="3" :tab="$t('高级组')"> <a-tab-pane key="3" :tab="$t('高级组')">
<PenGroups v-if="editor" :token="EDITOR_KEY" :current="current?.id" /> <PenGroups v-if="editor" :token="EDITOR_KEY" :current="current?.id" />
</a-tab-pane> </a-tab-pane>

View File

@ -29,13 +29,13 @@ const saveScene = async () => {
if (!json) return Promise.reject('无法获取场景数据'); if (!json) return Promise.reject('无法获取场景数据');
const res = await saveSceneById(props.id, json); const res = await saveSceneById(props.id, json);
if (!res) return Promise.reject('保存失败'); if (!res) return Promise.reject('保存失败');
// //
if (editor.value?.store) { if (editor.value?.store) {
editor.value.store.historyIndex = undefined; editor.value.store.historyIndex = undefined;
editor.value.store.histories = []; editor.value.store.histories = [];
} }
message.success(t('场景保存成功')); message.success(t('场景保存成功'));
return Promise.resolve(); return Promise.resolve();
}; };
@ -47,7 +47,6 @@ const pushScene = async () => {
return Promise.resolve(); return Promise.resolve();
}; };
//#endregion //#endregion
const title = ref<string>(''); const title = ref<string>('');
@ -162,9 +161,9 @@ const selectRobot = (id: string) => {
show-group-edit show-group-edit
/> />
</a-tab-pane> </a-tab-pane>
<a-tab-pane key="2" :tab="$t('库区')"> <!-- <a-tab-pane key="2" :tab="$t('库区')">
<PenGroups v-if="editor" :token="EDITOR_KEY" :current="current?.id" only-area1 /> <PenGroups v-if="editor" :token="EDITOR_KEY" :current="current?.id" only-area1 />
</a-tab-pane> </a-tab-pane> -->
<a-tab-pane key="3" :tab="$t('高级组')"> <a-tab-pane key="3" :tab="$t('高级组')">
<PenGroups v-if="editor" :token="EDITOR_KEY" :current="current?.id" /> <PenGroups v-if="editor" :token="EDITOR_KEY" :current="current?.id" />
</a-tab-pane> </a-tab-pane>

View File

@ -225,7 +225,7 @@ export class EditorService extends Meta2d {
#mapSceneArea(pen: MapPen): StandardSceneArea | null { #mapSceneArea(pen: MapPen): StandardSceneArea | null {
if (!pen.id || isEmpty(pen.area)) return null; if (!pen.id || isEmpty(pen.area)) return null;
const { id, label, desc, properties } = pen; const { id, label, desc, properties } = pen;
const { type, points, routes } = pen.area; const { type, points, routes, maxAmr } = pen.area;
const { x, y, width, height } = this.getPenRect(pen); const { x, y, width, height } = this.getPenRect(pen);
// 进行坐标转换:左上角原点 -> 中心点原点同时应用ratio缩放 // 进行坐标转换:左上角原点 -> 中心点原点同时应用ratio缩放
const transformedCoords = this.#transformCoordinate(x, y); const transformedCoords = this.#transformCoordinate(x, y);
@ -244,7 +244,7 @@ export class EditorService extends Meta2d {
if (MapAreaType. === type) { if (MapAreaType. === type) {
area.points = points?.filter((v) => this.getPenById(v)?.point?.type === MapPointType.); area.points = points?.filter((v) => this.getPenById(v)?.point?.type === MapPointType.);
} }
if ([MapAreaType., MapAreaType.].includes(type)) { if ([MapAreaType., MapAreaType., MapAreaType.].includes(type)) {
area.points = points?.filter((v) => { area.points = points?.filter((v) => {
const { point } = this.getPenById(v) ?? {}; const { point } = this.getPenById(v) ?? {};
if (isNil(point)) return false; if (isNil(point)) return false;
@ -255,6 +255,9 @@ export class EditorService extends Meta2d {
if (MapAreaType. === type) { if (MapAreaType. === type) {
area.routes = routes?.filter((v) => !isEmpty(this.getPenById(v)?.area)); area.routes = routes?.filter((v) => !isEmpty(this.getPenById(v)?.area));
} }
if (MapAreaType. === type) {
area.maxAmr = maxAmr;
}
return area; return area;
} }
//#endregion //#endregion
@ -777,6 +780,10 @@ export class EditorService extends Meta2d {
case MapAreaType.: case MapAreaType.:
selected?.filter(({ point }) => point?.type).forEach(({ id }) => points.push(id!)); selected?.filter(({ point }) => point?.type).forEach(({ id }) => points.push(id!));
break; break;
case MapAreaType.:
selected?.filter(({ point }) => point?.type).forEach(({ id }) => points.push(id!));
break;
default: default:
break; break;
} }