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

View File

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

View File

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

View File

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

View File

@ -1,10 +1,13 @@
$icons: ( $icons: (
area1-active, area1-active,
area1-detail,
area1, area1,
area2-active, area11-active,
area2, area11-detail,
area3-active, area11,
area3, area12-active,
area12-detail,
area12,
battery_charge, battery_charge,
battery, battery,
connect_off, connect_off,
@ -17,6 +20,7 @@ $icons: (
exit, exit,
pen, pen,
plus, plus,
point,
redo, redo,
register, register,
robot, 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", "stroke": "#595959",
"strokeActive": "#FCC947" "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" "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> </script>
<template> <template>
<a-card class="card-container" :bordered="false"> <a-card :bordered="false">
<template v-if="robot"> <template v-if="robot">
<a-row :gutter="[8, 8]"> <a-row :gutter="[8, 8]">
<a-col :span="24"> <a-col :span="24">
<a-flex align="center" :gap="8"> <a-flex align="center" :gap="8">
<i class="icon robot" /> <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-tag :bordered="false">{{ $t(RobotType[robot.type]) }}</a-tag>
</a-flex> </a-flex>
</a-col> </a-col>
@ -81,11 +81,6 @@ const stateDot = computed<string>(() => `state-${robot.value?.state}`);
</template> </template>
<style scoped lang="scss"> <style scoped lang="scss">
.ant-typography.title {
font-size: 20px;
line-height: 28px;
}
.dot { .dot {
width: 8px; width: 8px;
height: 8px; height: 8px;

View File

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

View File

@ -1,11 +1,12 @@
<script setup lang="ts"> <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 type { EditorService } from '@core/editor.service';
import { computed, inject, type InjectionKey, ref, type ShallowRef } from 'vue'; import { computed, inject, type InjectionKey, ref, type ShallowRef } from 'vue';
type Props = { type Props = {
token: InjectionKey<ShallowRef<EditorService>>; token: InjectionKey<ShallowRef<EditorService>>;
current?: string; current?: string;
onlyArea1?: boolean;
}; };
const props = defineProps<Props>(); const props = defineProps<Props>();
const editor = inject(props.token)!; const editor = inject(props.token)!;
@ -22,10 +23,12 @@ const points = computed<MapPen[]>(() =>
//#endregion //#endregion
//#region //#region
const areas = computed<MapPen[]>(() => editor.value.areas.value.filter(({ label }) => label?.includes(keyword.value)));
//#endregion //#endregion
</script> </script>
<template> <template>
<a-flex class="full" vertical>
<a-input class="search mb-16" :placeholder="$t('请输入搜索关键字')" v-model:value="keyword"> <a-input class="search mb-16" :placeholder="$t('请输入搜索关键字')" v-model:value="keyword">
<template #suffix> <template #suffix>
<i class="icon search size-16" /> <i class="icon search size-16" />
@ -37,9 +40,50 @@ const points = computed<MapPen[]>(() =>
<i class="icon dropdown" :class="{ active: v?.isActive }" /> <i class="icon dropdown" :class="{ active: v?.isActive }" />
</template> </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>
@ -106,5 +150,7 @@ const points = computed<MapPen[]>(() =>
</template> </template>
</a-list> </a-list>
</a-collapse-panel> </a-collapse-panel>
</template>
</a-collapse> </a-collapse>
</a-flex>
</template> </template>

View File

@ -1,7 +1,8 @@
<script setup lang="ts"> <script setup lang="ts">
import { getSceneById } from '@api/scene'; import { getSceneById } from '@api/scene';
import { EditorService } from '@core/editor.service'; 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 { ref } from 'vue';
import { onMounted, provide, shallowRef } from 'vue'; import { onMounted, provide, shallowRef } from 'vue';
@ -16,7 +17,7 @@ const props = defineProps<Props>();
const readScene = async () => { const readScene = async () => {
const res = await getSceneById(props.id); const res = await getSceneById(props.id);
title.value = res?.label ?? ''; title.value = res?.label ?? '';
editor.value?.load(res?.json, !editable.value); editor.value?.load(res?.json, editable.value);
}; };
//#endregion //#endregion
@ -42,13 +43,11 @@ const current = ref<{ type: 'robot' | 'point' | 'line' | 'area'; id: string }>()
watch( watch(
() => editor.value?.selected.value[0], () => editor.value?.selected.value[0],
(v) => { (v) => {
if (v) { const pen = editor.value?.getPenById(v);
const [pen] = editor.value?.find(v) ?? [];
if (pen?.id) { if (pen?.id) {
current.value = { type: <'point' | 'line' | 'area'>pen.name, id: pen.id }; current.value = { type: <'point' | 'line' | 'area'>pen.name, id: pen.id };
return; return;
} }
}
if (current.value?.type === 'robot') return; if (current.value?.type === 'robot') return;
current.value = undefined; current.value = undefined;
}, },
@ -97,7 +96,9 @@ const selectRobot = (id: string) => {
@change="selectRobot" @change="selectRobot"
/> />
</a-tab-pane> </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('高级组')"> <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>
@ -120,7 +121,17 @@ const selectRobot = (id: string) => {
<div v-if="show" class="card-container"> <div v-if="show" class="card-container">
<RobotDetailCard v-if="isRobot" :token="EDITOR_KEY" :current="current.id" /> <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> </div>
</template> </template>
</template> </template>

View File

@ -15,6 +15,17 @@ export class EditorService extends Meta2d {
const data = map ? JSON.parse(map) : undefined; const data = map ? JSON.parse(map) : undefined;
this.open(data); this.open(data);
this.setState(editable); 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 { public save(): string {
const data = this.data(); const data = this.data();
@ -26,7 +37,7 @@ export class EditorService extends Meta2d {
} }
public setState(editable?: boolean): void { public setState(editable?: boolean): void {
this.lock(editable ? LockState.None : LockState.Disable); this.lock(editable ? LockState.None : LockState.DisableMoveScale);
} }
public override data(): SceneData { public override data(): SceneData {
@ -99,7 +110,7 @@ export class EditorService extends Meta2d {
public createRobotGroup(): void { public createRobotGroup(): void {
const id = s8(); const id = s8();
const label = `RG-${id}`; const label = `RG${id}`;
const groups = clone(this.#robotGroups$$.value); const groups = clone(this.#robotGroups$$.value);
groups.push({ id, label }); groups.push({ id, label });
this.#robotGroups$$.next(groups); this.#robotGroups$$.next(groups);
@ -145,6 +156,10 @@ export class EditorService extends Meta2d {
public override find(target: string): MapPen[] { public override find(target: string): MapPen[] {
return super.find(target); 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 { public override active(target: string | Pen[], emit?: boolean): void {
const pens = isString(target) ? this.find(target) : target; const pens = isString(target) ? this.find(target) : target;
@ -175,7 +190,7 @@ export class EditorService extends Meta2d {
id, id,
name: 'point', name: 'point',
tags: ['point', `point-${type}`], tags: ['point', `point-${type}`],
label: `P-${id}`, label: `P${id}`,
point: { type }, point: { type },
}; };
const { x, y, width, height } = this.getPenRect(pen); const { x, y, width, height } = this.getPenRect(pen);
@ -200,27 +215,69 @@ export class EditorService extends Meta2d {
//#endregion //#endregion
//#region 线路 //#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 //#endregion
//#region 区域 //#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.) { public async addArea(p1: Point, p2: Point, type = MapAreaType.) {
const scale = this.data().scale ?? 1; const scale = this.data().scale ?? 1;
const w = Math.abs(p1.x - p2.x); const w = Math.abs(p1.x - p2.x);
const h = Math.abs(p1.y - p2.y); const h = Math.abs(p1.y - p2.y);
if (w * scale < 50 || h * scale < 60) return; 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 = { const pen: MapPen = {
id,
name: 'area', name: 'area',
tags: ['area', `area-${type}`], tags: ['area', `area-${type}`],
label: `A${id}`,
x: Math.min(p1.x, p2.x), x: Math.min(p1.x, p2.x),
y: Math.min(p1.y, p2.y), y: Math.min(p1.y, p2.y),
width: w, width: w,
height: h, height: h,
area: { type }, lineWidth: 1,
area: { type, points, routes },
locked: LockState.DisableMoveScale, locked: LockState.DisableMoveScale,
}; };
const area = await this.addPen(pen, false, true, true); const area = await this.addPen(pen, false, true, true);
this.bottom(area); this.bottom(area);
this.pushHistory({ type: EditType.Add, pens: [cloneDeep(pen)] }); // this.pushHistory({ type: EditType.Add, pens: [cloneDeep(pen)] });
} }
//#endregion //#endregion
@ -399,13 +456,23 @@ function drawLine(ctx: CanvasRenderingContext2D, pen: MapPen): void {
} }
function drawArea(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 { type } = pen.area ?? {};
const { label = '' } = pen ?? {};
ctx.save(); ctx.save();
ctx.lineWidth = 1; ctx.rect(x, y, w, h);
ctx.strokeRect(x, y, width, height); ctx.fillStyle = get(theme, `area.fill-${type}`) ?? '';
ctx.fillStyle = '#fff'; ctx.fill();
ctx.fillText(String(type), x + width / 2, y); 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(); ctx.restore();
} }
//#endregion //#endregion