This commit is contained in:
chndfang 2025-05-06 23:48:21 +08:00
parent d3f5387df9
commit fe1ba733ef
21 changed files with 464 additions and 108 deletions

View File

@ -102,10 +102,14 @@ interface MapPen {
interface MapPointInfo {
type: MapPointType; // 点位类型
robots?: Array<RobotInfo['id']>; // 绑定机器人id集合
actions?: Array<string>; // 绑定动作点id集合
isBlock?: boolean; // 是否禁行
isForbidAvoid?: boolean; // 是否禁止避让
}
interface MapRouteInfo {
type: MapRouteType; // 线路类型
direction?: -1 | 1; // 线路方向
direction?: -1 | 1; // 方向
pass?: MapRoutePassType; // 可通行类型
}
interface MapAreaInfo {
type: MapAreaType; // 区域类型
@ -128,10 +132,20 @@ enum MapPointType {
障碍点 = 99, // 待优化,后续将单独抽离
}
enum MapRouteType {
直线 = 'line',
三阶贝塞尔曲线 = 'bezier3',
}
enum MapRoutePassType {
无,
仅空载可通行,
仅载货可通行,
禁行 = 10,
}
enum MapAreaType {
库区 = 1,

View File

@ -650,6 +650,11 @@
color: get-color(text4);
}
&.card-title {
font-size: 20px;
line-height: 28px;
}
& > strong {
font: 500 16px/22px Roboto;
}

View File

@ -27,6 +27,13 @@ export enum MapRouteType {
线 = 'bezier3',
}
export const MAP_ROUTE_TYPES = Object.freeze(<[string, MapRouteType][]>Object.entries(MapRouteType));
export enum MapRoutePassType {
,
,
,
= 10,
}
//#endregion
//#region 区域

View File

@ -1,7 +1,7 @@
import type { RobotInfo } from '@api/robot';
import type { Pen } from '@meta2d/core';
import type { MapAreaType, MapPointType, MapRouteType } from './constant';
import type { MapAreaType, MapPointType, MapRoutePassType, MapRouteType } from './constant';
export interface MapPen extends Pen {
label?: string; // 展示名称
@ -19,13 +19,17 @@ export interface MapPen extends Pen {
export interface MapPointInfo {
type: MapPointType; // 点位类型
robots?: Array<RobotInfo['id']>; // 绑定机器人id集合
actions?: Array<string>; // 绑定动作点id集合
isBlock?: boolean; // 是否禁行
isForbidAvoid?: boolean; // 是否禁止避让
}
//#endregion
//#region 线路
export interface MapRouteInfo {
type: MapRouteType; // 线路类型
direction?: -1 | 1; // 线路方向
direction?: -1 | 1; // 方向
pass?: MapRoutePassType; // 可通行类型
}
//#endregion

View File

@ -1,10 +1,13 @@
$icons: (
area1-active,
area1-detail,
area1,
area2-active,
area2,
area3-active,
area3,
area11-active,
area11-detail,
area11,
area12-active,
area12-detail,
area12,
battery_charge,
battery,
connect_off,
@ -17,6 +20,7 @@ $icons: (
exit,
pen,
plus,
point,
redo,
register,
robot,

Binary file not shown.

After

Width:  |  Height:  |  Size: 590 B

View File

Before

Width:  |  Height:  |  Size: 1.3 KiB

After

Width:  |  Height:  |  Size: 1.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

View File

Before

Width:  |  Height:  |  Size: 1.1 KiB

After

Width:  |  Height:  |  Size: 1.1 KiB

View File

Before

Width:  |  Height:  |  Size: 1.2 KiB

After

Width:  |  Height:  |  Size: 1.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

View File

Before

Width:  |  Height:  |  Size: 1.0 KiB

After

Width:  |  Height:  |  Size: 1.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 KiB

View File

@ -13,5 +13,14 @@
"stroke": "#595959",
"strokeActive": "#FCC947"
},
"area": {
"stroke-1": "#9ACDFF99",
"fill-1": "#9ACDFF33",
"stroke-11": "#FF535399",
"fill-11": "#FF9A9A33",
"stroke-12": "#0DBB8A99",
"fill-12": "#0DBB8A33",
"strokeActive": "#FCC947"
},
"line": "#8C8C8C"
}

View File

@ -0,0 +1,79 @@
<script setup lang="ts">
import { type MapAreaInfo, MapAreaType, type MapPen } from '@api/map';
import type { EditorService } from '@core/editor.service';
import sTheme from '@core/theme.service';
import { computed, inject, type InjectionKey, type ShallowRef } from 'vue';
type Props = {
token: InjectionKey<ShallowRef<EditorService>>;
current?: string;
};
const props = defineProps<Props>();
const editor = inject(props.token)!;
const pen = computed<MapPen | undefined>(() => editor.value.getPenById(props.current));
const area = computed<MapAreaInfo | null>(() => {
const area = pen.value?.area;
if (!area?.type) return null;
return area;
});
const icon = computed<string>(() => `area${area.value?.type}-detail`);
const bindPoint = computed<string>(
() =>
area.value?.points
?.map((v) => editor.value.getPenById(v)?.label)
.filter((v) => !!v)
.join('、') ?? '',
);
const bindRoute = computed<string>(
() =>
area.value?.routes
?.map((v) => editor.value.getRouteLabel(v))
.filter((v) => !!v)
.join('、') ?? '',
);
</script>
<template>
<a-card :bordered="false">
<template v-if="pen && area">
<a-row :gutter="[8, 8]">
<a-col :span="24">
<a-flex align="center" :gap="8">
<i class="icon" :class="icon" />
<a-typography-text class="card-title" style="flex: auto" :content="pen.label" ellipsis />
<a-tag :bordered="false">{{ $t(MapAreaType[area.type]) }}</a-tag>
</a-flex>
</a-col>
<a-col :span="24">
<a-typography-text code>{{ pen.desc || $t('暂无描述') }}</a-typography-text>
</a-col>
</a-row>
<a-list class="block mt-16">
<a-list-item v-if="MapAreaType.库区 === area.type">
<a-flex :gap="8" vertical>
<a-typography-text type="secondary">{{ $t('绑定动作点') }}</a-typography-text>
<a-typography-text>{{ bindPoint || $t('暂无') }}</a-typography-text>
</a-flex>
</a-list-item>
<a-list-item v-if="[MapAreaType.互斥区, MapAreaType.非互斥区].includes(area.type)">
<a-flex :gap="8" vertical>
<a-typography-text type="secondary">{{ $t('绑定站点') }}</a-typography-text>
<a-typography-text>{{ bindPoint || $t('暂无') }}</a-typography-text>
</a-flex>
</a-list-item>
<a-list-item v-if="MapAreaType.互斥区 === area.type">
<a-flex :gap="8" vertical>
<a-typography-text type="secondary">{{ $t('绑定路段') }}</a-typography-text>
<a-typography-text>{{ bindRoute || $t('暂无') }}</a-typography-text>
</a-flex>
</a-list-item>
</a-list>
</template>
<a-empty v-else :image="sTheme.empty" />
</a-card>
</template>

View File

@ -0,0 +1,115 @@
<script setup lang="ts">
import { MapAreaType, type MapPen, type MapPointInfo, MapPointType } from '@api/map';
import type { EditorService } from '@core/editor.service';
import sTheme from '@core/theme.service';
import { computed, inject, type InjectionKey, type ShallowRef } from 'vue';
type Props = {
token: InjectionKey<ShallowRef<EditorService>>;
current?: string;
};
const props = defineProps<Props>();
const editor = inject(props.token)!;
const pen = computed<MapPen | undefined>(() => editor.value.getPenById(props.current));
const point = computed<MapPointInfo | null>(() => {
const point = pen.value?.point;
if (!point?.type) return null;
return point;
});
const bindRobot = computed<string>(
() =>
point.value?.robots
?.map((v) => editor.value.getRobotById(v)?.label)
.filter((v) => !!v)
.join('、') ?? '',
);
const bindAction = computed<string>(
() =>
point.value?.actions
?.map((v) => editor.value.getPenById(v)?.label)
.filter((v) => !!v)
.join('、') ?? '',
);
const mapAreas = (type: MapAreaType): string => {
const id = pen.value?.id;
if (!id) return '';
return (
editor.value
.find(`area-${type}`)
.filter(({ area }) => area?.points?.includes(id))
?.map(({ label }) => label)
.filter((v) => !!v)
.join('、') ?? ''
);
};
const coArea1 = computed<string>(() => mapAreas(MapAreaType.库区));
const coArea2 = computed<string>(() => mapAreas(MapAreaType.互斥区));
</script>
<template>
<a-card :bordered="false">
<template v-if="pen && point">
<a-row :gutter="[8, 8]">
<a-col :span="24">
<a-flex align="center" :gap="8">
<i class="icon point" />
<a-typography-text class="card-title" style="flex: auto" :content="pen.label" ellipsis />
<a-tag :bordered="false">{{ $t(MapPointType[point.type]) }}</a-tag>
</a-flex>
</a-col>
<a-col :span="24">
<a-typography-text code>{{ pen.desc || $t('暂无描述') }}</a-typography-text>
</a-col>
</a-row>
<a-list class="block mt-16">
<a-list-item>
<a-typography-text type="secondary">{{ $t('站点坐标') }}</a-typography-text>
<a-typography-text>({{ pen.x?.toFixed() }},{{ pen.y?.toFixed() }})</a-typography-text>
</a-list-item>
<a-list-item v-if="[MapPointType.充电点, MapPointType.停靠点].includes(point.type)">
<a-flex :gap="8" vertical>
<a-typography-text type="secondary">{{ $t('绑定机器人') }}</a-typography-text>
<a-typography-text>{{ bindRobot || $t('暂无') }}</a-typography-text>
</a-flex>
</a-list-item>
<a-list-item v-if="MapPointType.等待点 === point.type">
<a-flex :gap="8" vertical>
<a-typography-text type="secondary">{{ $t('绑定动作点') }}</a-typography-text>
<a-typography-text>{{ bindAction || $t('暂无') }}</a-typography-text>
</a-flex>
</a-list-item>
<a-list-item v-if="MapPointType.动作点 === point.type">
<a-flex :gap="8" vertical>
<a-typography-text type="secondary">{{ $t('关联库区') }}</a-typography-text>
<a-typography-text>{{ coArea1 || $t('暂无') }}</a-typography-text>
</a-flex>
</a-list-item>
<a-list-item
v-if="
[
MapPointType.普通点,
MapPointType.电梯点,
MapPointType.自动门点,
MapPointType.等待点,
MapPointType.充电点,
MapPointType.停靠点,
MapPointType.动作点,
MapPointType.临时避让点,
].includes(point.type)
"
>
<a-flex :gap="8" vertical>
<a-typography-text type="secondary">{{ $t('关联互斥区') }}</a-typography-text>
<a-typography-text>{{ coArea2 || $t('暂无') }}</a-typography-text>
</a-flex>
</a-list-item>
</a-list>
</template>
<a-empty v-else :image="sTheme.empty" />
</a-card>
</template>

View File

@ -22,13 +22,13 @@ const stateDot = computed<string>(() => `state-${robot.value?.state}`);
</script>
<template>
<a-card class="card-container" :bordered="false">
<a-card :bordered="false">
<template v-if="robot">
<a-row :gutter="[8, 8]">
<a-col :span="24">
<a-flex align="center" :gap="8">
<i class="icon robot" />
<a-typography-text class="title" style="flex: auto" :content="robot.label" ellipsis />
<a-typography-text class="card-title" style="flex: auto" :content="robot.label" ellipsis />
<a-tag :bordered="false">{{ $t(RobotType[robot.type]) }}</a-tag>
</a-flex>
</a-col>
@ -81,11 +81,6 @@ const stateDot = computed<string>(() => `state-${robot.value?.state}`);
</template>
<style scoped lang="scss">
.ant-typography.title {
font-size: 20px;
line-height: 28px;
}
.dot {
width: 8px;
height: 8px;

View File

@ -38,7 +38,7 @@ watch(editor.value.mouseBrush, (v) => {
size="large"
@click="mode = MapAreaType.互斥区"
>
<i class="icon" :class="mode === MapAreaType.互斥区 ? 'area2-active' : 'area2'" />
<i class="icon" :class="mode === MapAreaType.互斥区 ? 'area11-active' : 'area11'" />
</a-button>
<a-button
class="icon-btn tool-btn ml-12"
@ -46,7 +46,7 @@ watch(editor.value.mouseBrush, (v) => {
size="large"
@click="mode = MapAreaType.非互斥区"
>
<i class="icon" :class="mode === MapAreaType.非互斥区 ? 'area3-active' : 'area3'" />
<i class="icon" :class="mode === MapAreaType.非互斥区 ? 'area12-active' : 'area12'" />
</a-button>
<a-divider class="size-24 mh-8" type="vertical" />

View File

@ -1,11 +1,12 @@
<script setup lang="ts">
import { type MapPen, MapPointType } from '@api/map';
import { MapAreaType, type MapPen, MapPointType } from '@api/map';
import type { EditorService } from '@core/editor.service';
import { computed, inject, type InjectionKey, ref, type ShallowRef } from 'vue';
type Props = {
token: InjectionKey<ShallowRef<EditorService>>;
current?: string;
onlyArea1?: boolean;
};
const props = defineProps<Props>();
const editor = inject(props.token)!;
@ -22,89 +23,134 @@ const points = computed<MapPen[]>(() =>
//#endregion
//#region
const areas = computed<MapPen[]>(() => editor.value.areas.value.filter(({ label }) => label?.includes(keyword.value)));
//#endregion
</script>
<template>
<a-input class="search mb-16" :placeholder="$t('请输入搜索关键字')" v-model:value="keyword">
<template #suffix>
<i class="icon search size-16" />
</template>
</a-input>
<a-flex class="full" vertical>
<a-input class="search mb-16" :placeholder="$t('请输入搜索关键字')" v-model:value="keyword">
<template #suffix>
<i class="icon search size-16" />
</template>
</a-input>
<a-collapse style="flex: auto; overflow-y: auto" expand-icon-position="end" ghost>
<template #expandIcon="v">
<i class="icon dropdown" :class="{ active: v?.isActive }" />
</template>
<a-collapse style="flex: auto; overflow-y: auto" expand-icon-position="end" ghost>
<template #expandIcon="v">
<i class="icon dropdown" :class="{ active: v?.isActive }" />
</template>
<a-collapse-panel :header="$t('互斥区')"></a-collapse-panel>
<a-collapse-panel v-if="onlyArea1" :header="$t('库区')">
<a-list rowKey="id" :data-source="areas.filter(({ area }) => area?.type === MapAreaType.库区)">
<template #renderItem="{ item }">
<a-list-item
class="ph-16"
:class="{ selected: item.id === current }"
style="height: 36px"
@click="editor.active(item.id)"
>
<a-typography-text type="secondary">{{ item.label }}</a-typography-text>
</a-list-item>
</template>
</a-list>
</a-collapse-panel>
<template v-else>
<a-collapse-panel :header="$t('互斥区')">
<a-list rowKey="id" :data-source="areas.filter(({ area }) => area?.type === MapAreaType.互斥区)">
<template #renderItem="{ item }">
<a-list-item
class="ph-16"
:class="{ selected: item.id === current }"
style="height: 36px"
@click="editor.active(item.id)"
>
<a-typography-text type="secondary">{{ item.label }}</a-typography-text>
</a-list-item>
</template>
</a-list>
</a-collapse-panel>
<a-collapse-panel :header="$t('非互斥区')"></a-collapse-panel>
<a-collapse-panel :header="$t('非互斥区')">
<a-list rowKey="id" :data-source="areas.filter(({ area }) => area?.type === MapAreaType.非互斥区)">
<template #renderItem="{ item }">
<a-list-item
class="ph-16"
:class="{ selected: item.id === current }"
style="height: 36px"
@click="editor.active(item.id)"
>
<a-typography-text type="secondary">{{ item.label }}</a-typography-text>
</a-list-item>
</template>
</a-list>
</a-collapse-panel>
<a-collapse-panel :header="$t('仅载货可通行路线')"></a-collapse-panel>
<a-collapse-panel :header="$t('仅载货可通行路线')"></a-collapse-panel>
<a-collapse-panel :header="$t('仅空载可通行路线')"></a-collapse-panel>
<a-collapse-panel :header="$t('仅空载可通行路线')"></a-collapse-panel>
<a-collapse-panel :header="$t('禁行路线')"></a-collapse-panel>
<a-collapse-panel :header="$t('禁行路线')"></a-collapse-panel>
<a-collapse-panel :header="$t('等待点')">
<a-list rowKey="id" :data-source="points.filter(({ point }) => point?.type === MapPointType.等待点)">
<template #renderItem="{ item }">
<a-list-item
class="ph-16"
:class="{ selected: item.id === current }"
style="height: 36px"
@click="editor.active(item.id)"
>
<a-typography-text type="secondary">{{ item.label }}</a-typography-text>
</a-list-item>
</template>
</a-list>
</a-collapse-panel>
<a-collapse-panel :header="$t('等待点')">
<a-list rowKey="id" :data-source="points.filter(({ point }) => point?.type === MapPointType.等待点)">
<template #renderItem="{ item }">
<a-list-item
class="ph-16"
:class="{ selected: item.id === current }"
style="height: 36px"
@click="editor.active(item.id)"
>
<a-typography-text type="secondary">{{ item.label }}</a-typography-text>
</a-list-item>
</template>
</a-list>
</a-collapse-panel>
<a-collapse-panel :header="$t('充电点')">
<a-list rowKey="id" :data-source="points.filter(({ point }) => point?.type === MapPointType.充电点)">
<template #renderItem="{ item }">
<a-list-item
class="ph-16"
:class="{ selected: item.id === current }"
style="height: 36px"
@click="editor.active(item.id)"
>
<a-typography-text type="secondary">{{ item.label }}</a-typography-text>
</a-list-item>
</template>
</a-list>
</a-collapse-panel>
<a-collapse-panel :header="$t('充电点')">
<a-list rowKey="id" :data-source="points.filter(({ point }) => point?.type === MapPointType.充电点)">
<template #renderItem="{ item }">
<a-list-item
class="ph-16"
:class="{ selected: item.id === current }"
style="height: 36px"
@click="editor.active(item.id)"
>
<a-typography-text type="secondary">{{ item.label }}</a-typography-text>
</a-list-item>
</template>
</a-list>
</a-collapse-panel>
<a-collapse-panel :header="$t('停靠点')">
<a-list rowKey="id" :data-source="points.filter(({ point }) => point?.type === MapPointType.停靠点)">
<template #renderItem="{ item }">
<a-list-item
class="ph-16"
:class="{ selected: item.id === current }"
style="height: 36px"
@click="editor.active(item.id)"
>
<a-typography-text type="secondary">{{ item.label }}</a-typography-text>
</a-list-item>
</template>
</a-list>
</a-collapse-panel>
<a-collapse-panel :header="$t('停靠点')">
<a-list rowKey="id" :data-source="points.filter(({ point }) => point?.type === MapPointType.停靠点)">
<template #renderItem="{ item }">
<a-list-item
class="ph-16"
:class="{ selected: item.id === current }"
style="height: 36px"
@click="editor.active(item.id)"
>
<a-typography-text type="secondary">{{ item.label }}</a-typography-text>
</a-list-item>
</template>
</a-list>
</a-collapse-panel>
<a-collapse-panel :header="$t('禁行点')">
<a-list rowKey="id" :data-source="points.filter(({ point }) => point?.type === MapPointType.禁行点)">
<template #renderItem="{ item }">
<a-list-item
class="ph-16"
:class="{ selected: item.id === current }"
style="height: 36px"
@click="editor.active(item.id)"
>
<a-typography-text type="secondary">{{ item.label }}</a-typography-text>
</a-list-item>
</template>
</a-list>
</a-collapse-panel>
</a-collapse>
<a-collapse-panel :header="$t('禁行点')">
<a-list rowKey="id" :data-source="points.filter(({ point }) => point?.type === MapPointType.禁行点)">
<template #renderItem="{ item }">
<a-list-item
class="ph-16"
:class="{ selected: item.id === current }"
style="height: 36px"
@click="editor.active(item.id)"
>
<a-typography-text type="secondary">{{ item.label }}</a-typography-text>
</a-list-item>
</template>
</a-list>
</a-collapse-panel>
</template>
</a-collapse>
</a-flex>
</template>

View File

@ -1,7 +1,8 @@
<script setup lang="ts">
import { getSceneById } from '@api/scene';
import { EditorService } from '@core/editor.service';
import { computed, nextTick, watch } from 'vue';
import { isNil } from 'lodash-es';
import { computed, watch } from 'vue';
import { ref } from 'vue';
import { onMounted, provide, shallowRef } from 'vue';
@ -16,7 +17,7 @@ const props = defineProps<Props>();
const readScene = async () => {
const res = await getSceneById(props.id);
title.value = res?.label ?? '';
editor.value?.load(res?.json, !editable.value);
editor.value?.load(res?.json, editable.value);
};
//#endregion
@ -42,12 +43,10 @@ const current = ref<{ type: 'robot' | 'point' | 'line' | 'area'; id: string }>()
watch(
() => editor.value?.selected.value[0],
(v) => {
if (v) {
const [pen] = editor.value?.find(v) ?? [];
if (pen?.id) {
current.value = { type: <'point' | 'line' | 'area'>pen.name, id: pen.id };
return;
}
const pen = editor.value?.getPenById(v);
if (pen?.id) {
current.value = { type: <'point' | 'line' | 'area'>pen.name, id: pen.id };
return;
}
if (current.value?.type === 'robot') return;
current.value = undefined;
@ -97,7 +96,9 @@ const selectRobot = (id: string) => {
@change="selectRobot"
/>
</a-tab-pane>
<a-tab-pane key="2" :tab="$t('库区')">Content of Tab Pane 2</a-tab-pane>
<a-tab-pane key="2" :tab="$t('库区')">
<PenGroups v-if="editor" :token="EDITOR_KEY" :current="current?.id" only-area1 />
</a-tab-pane>
<a-tab-pane key="3" :tab="$t('高级组')">
<PenGroups v-if="editor" :token="EDITOR_KEY" :current="current?.id" />
</a-tab-pane>
@ -120,7 +121,17 @@ const selectRobot = (id: string) => {
<div v-if="show" class="card-container">
<RobotDetailCard v-if="isRobot" :token="EDITOR_KEY" :current="current.id" />
<template v-if="isPoint"> </template>
<template v-if="isPoint">
<div v-if="editable"></div>
<PointDetailCard v-else :token="EDITOR_KEY" :current="current.id" />
</template>
<template v-if="isRoute">
<div v-if="editable"></div>
</template>
<template v-if="isArea">
<div v-if="editable"></div>
<AreaDetailCard v-else :token="EDITOR_KEY" :current="current.id" />
</template>
</div>
</template>
</template>

View File

@ -15,6 +15,17 @@ export class EditorService extends Meta2d {
const data = map ? JSON.parse(map) : undefined;
this.open(data);
this.setState(editable);
this.addPoint({ x: 100, y: 100 }, 1);
this.addPoint({ x: 200, y: 100 }, 2);
this.addPoint({ x: 300, y: 100 }, 3);
this.addPoint({ x: 400, y: 100 }, 4);
this.addPoint({ x: 100, y: 200 }, 11);
this.addPoint({ x: 200, y: 200 }, 12);
this.addPoint({ x: 300, y: 200 }, 13);
this.addPoint({ x: 400, y: 200 }, 14);
this.addPoint({ x: 500, y: 200 }, 15);
this.addPoint({ x: 600, y: 200 }, 16);
}
public save(): string {
const data = this.data();
@ -26,7 +37,7 @@ export class EditorService extends Meta2d {
}
public setState(editable?: boolean): void {
this.lock(editable ? LockState.None : LockState.Disable);
this.lock(editable ? LockState.None : LockState.DisableMoveScale);
}
public override data(): SceneData {
@ -99,7 +110,7 @@ export class EditorService extends Meta2d {
public createRobotGroup(): void {
const id = s8();
const label = `RG-${id}`;
const label = `RG${id}`;
const groups = clone(this.#robotGroups$$.value);
groups.push({ id, label });
this.#robotGroups$$.next(groups);
@ -145,6 +156,10 @@ export class EditorService extends Meta2d {
public override find(target: string): MapPen[] {
return super.find(target);
}
public getPenById(id?: string): MapPen | undefined {
if (!id) return;
return this.find(id)[0];
}
public override active(target: string | Pen[], emit?: boolean): void {
const pens = isString(target) ? this.find(target) : target;
@ -175,7 +190,7 @@ export class EditorService extends Meta2d {
id,
name: 'point',
tags: ['point', `point-${type}`],
label: `P-${id}`,
label: `P${id}`,
point: { type },
};
const { x, y, width, height } = this.getPenRect(pen);
@ -200,27 +215,69 @@ export class EditorService extends Meta2d {
//#endregion
//#region 线路
public getRouteLabel(id?: string): string {
if (!id) return '';
const pen = this.getPenById(id);
if (isNil(pen)) return '';
const [a1, a2] = pen.anchors ?? [];
if (!a1?.connectTo || !a2?.connectTo) return '';
const p1 = this.getPenById(a1.connectTo);
const p2 = this.getPenById(a2.connectTo);
if (isNil(p1) || isNil(p2)) return '';
const { direction = 1 } = pen.route ?? {};
return `${p1.text} ${direction > 0 ? '→' : '←'} ${p2.text}`;
}
//#endregion
//#region 区域
public readonly areas = useObservable<MapPen[], MapPen[]>(
this.#change$$.pipe(
filter((v) => v),
debounceTime(100),
map(() => this.find('area')),
),
{ initialValue: new Array<MapPen>() },
);
public async addArea(p1: Point, p2: Point, type = MapAreaType.) {
const scale = this.data().scale ?? 1;
const w = Math.abs(p1.x - p2.x);
const h = Math.abs(p1.y - p2.y);
if (w * scale < 50 || h * scale < 60) return;
const selected = <MapPen[]>this.store.active;
const id = s8();
const points = new Array<string>();
const routes = new Array<string>();
switch (type) {
case MapAreaType.:
selected?.filter(({ point }) => point?.type === MapPointType.).forEach(({ id }) => points.push(id!));
break;
case MapAreaType.:
selected?.filter(({ point }) => point?.type).forEach(({ id }) => points.push(id!));
selected?.filter(({ route }) => route?.type).forEach(({ id }) => routes.push(id!));
break;
case MapAreaType.:
selected?.filter(({ point }) => point?.type).forEach(({ id }) => points.push(id!));
break;
default:
break;
}
const pen: MapPen = {
id,
name: 'area',
tags: ['area', `area-${type}`],
label: `A${id}`,
x: Math.min(p1.x, p2.x),
y: Math.min(p1.y, p2.y),
width: w,
height: h,
area: { type },
lineWidth: 1,
area: { type, points, routes },
locked: LockState.DisableMoveScale,
};
const area = await this.addPen(pen, false, true, true);
this.bottom(area);
this.pushHistory({ type: EditType.Add, pens: [cloneDeep(pen)] });
// this.pushHistory({ type: EditType.Add, pens: [cloneDeep(pen)] });
}
//#endregion
@ -399,13 +456,23 @@ function drawLine(ctx: CanvasRenderingContext2D, pen: MapPen): void {
}
function drawArea(ctx: CanvasRenderingContext2D, pen: MapPen): void {
const { x = 0, y = 0, width = 0, height = 0 } = pen.calculative?.worldRect ?? {};
const theme = sTheme.editor;
const { active, fontSize = 14, lineHeight = 1.5, fontFamily } = pen.calculative ?? {};
const { x = 0, y = 0, width: w = 0, height: h = 0 } = pen.calculative?.worldRect ?? {};
const { type } = pen.area ?? {};
const { label = '' } = pen ?? {};
ctx.save();
ctx.lineWidth = 1;
ctx.strokeRect(x, y, width, height);
ctx.fillStyle = '#fff';
ctx.fillText(String(type), x + width / 2, y);
ctx.rect(x, y, w, h);
ctx.fillStyle = get(theme, `area.fill-${type}`) ?? '';
ctx.fill();
ctx.strokeStyle = get(theme, active ? 'area.strokeActive' : `area.stroke-${type}`) ?? '';
ctx.stroke();
ctx.fillStyle = get(theme, 'color') ?? '';
ctx.font = `${fontSize}px/${lineHeight} ${fontFamily}`;
ctx.textAlign = 'center';
ctx.textBaseline = 'top';
ctx.fillText(label, x + w / 2, y - fontSize * lineHeight);
ctx.restore();
}
//#endregion