This commit is contained in:
chndfang 2025-05-09 20:15:04 +08:00
parent 081764b4b1
commit 43f16303d8
9 changed files with 293 additions and 17 deletions

File diff suppressed because one or more lines are too long

View File

@ -417,6 +417,7 @@
}
.ant-list.block .ant-list-item {
gap: 8px;
padding: 12px;
background-color: get-color(fill4);
border: none;

View File

@ -36,6 +36,9 @@ export enum MapRoutePassType {
,
= 10,
}
export const MAP_ROUTE_PASS_TYPES = Object.freeze(
<[string, MapRoutePassType][]>Object.entries(MapRoutePassType).filter(([, v]) => typeof v === 'number'),
);
//#endregion
//#region 区域

View File

@ -13,9 +13,9 @@ 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 v = pen.value?.area;
if (!v?.type) return null;
return v;
});
const icon = computed<string>(() => `area${area.value?.type}-detail`);

View File

@ -13,9 +13,9 @@ 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 v = pen.value?.point;
if (!v?.type) return null;
return v;
});
const bindRobot = computed<string>(

View File

@ -0,0 +1,59 @@
<script setup lang="ts">
import { MAP_ROUTE_TYPE, type MapPen, type MapRouteInfo, MapRoutePassType } 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>>;
editable?: boolean;
current?: string;
};
const props = defineProps<Props>();
const editor = inject(props.token)!;
const pen = computed<MapPen | undefined>(() => editor.value.getPenById(props.current));
const route = computed<MapRouteInfo | null>(() => {
const v = pen.value?.route;
if (!v?.type) return null;
return v;
});
const label = computed<string>(() => editor.value.getRouteLabel(pen.value?.id));
</script>
<template>
<a-card :bordered="false">
<template v-if="pen && route">
<a-row :gutter="[8, 8]">
<a-col :span="24">
<a-flex align="center" :gap="8">
<i class="icon route" />
<a-typography-text class="card-title" style="flex: auto" :content="label" ellipsis />
<a-tag :bordered="false">{{ $t(MapRoutePassType[route.pass ?? MapRoutePassType.]) }}</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>{{ $t(MAP_ROUTE_TYPE[route.type]) }}</a-typography-text>
</a-list-item>
<a-list-item>
<a-typography-text type="secondary">{{ $t('路段长度') }}</a-typography-text>
<a-typography-text>{{ pen.length?.toFixed() }}</a-typography-text>
</a-list-item>
<a-list-item>
<a-typography-text style="flex: none" type="secondary">{{ $t('路段方向') }}</a-typography-text>
<a-typography-text :content="label" ellipsis />
</a-list-item>
</a-list>
</template>
<a-empty v-else :image="sTheme.empty" />
</a-card>
</template>

View File

@ -0,0 +1,163 @@
<script setup lang="ts">
import {
MAP_ROUTE_PASS_TYPES,
MAP_ROUTE_TYPES,
type MapPen,
type MapRouteInfo,
MapRoutePassType,
MapRouteType,
} 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>>;
id?: string;
};
const props = defineProps<Props>();
const editor = inject(props.token)!;
const pen = computed<MapPen | null>(() => {
const v = editor.value.current.value;
if (v?.id !== props.id) return null;
return v!;
});
const route = computed<MapRouteInfo | null>(() => {
const v = pen.value?.route;
if (!v?.type) return null;
return v;
});
</script>
<template>
<a-card class="full" :title="$t('属性')" :bordered="false">
<a-flex v-if="id && pen && route" :gap="24" vertical>
<a-row :gutter="[8, 8]">
<a-col :span="24">
<a-select
:value="route.pass ?? MapRoutePassType.无"
@change="editor.updateRoute(id, { pass: <number>$event })"
>
<a-select-option v-for="[l, v] in MAP_ROUTE_PASS_TYPES" :key="v">{{ $t(l) }}</a-select-option>
</a-select>
</a-col>
</a-row>
<a-row :gutter="[8, 8]">
<a-col :span="24">
<a-typography-text>{{ $t('描述') }}:</a-typography-text>
</a-col>
<a-col :span="24">
<a-textarea
class="prop"
:placeholder="$t('请输入描述内容')"
:maxlength="100"
:autoSize="{ minRows: 3, maxRows: 3 }"
:value="pen?.desc"
@change="editor.updatePen(id, { desc: $event.target.value }, false)"
/>
</a-col>
</a-row>
<a-row align="middle" :gutter="10" :wrap="false">
<a-col flex="none">
<a-typography-text>{{ $t('路段长度') }}:</a-typography-text>
</a-col>
<a-col flex="auto">
<a-input :value="pen.length?.toFixed()" disabled />
</a-col>
</a-row>
<a-row align="middle" :gutter="10" :wrap="false">
<a-col flex="none">
<a-typography-text>{{ $t('路段方向') }}:</a-typography-text>
</a-col>
<a-col flex="auto">
<a-select :value="route.direction || 1" @change="editor.updateRoute(id, { direction: <-1 | 1>$event })">
<a-select-option :value="1">{{ editor.getRouteLabel(id, 1) }}</a-select-option>
<a-select-option :value="-1">{{ editor.getRouteLabel(id, -1) }}</a-select-option>
</a-select>
</a-col>
</a-row>
<a-row align="middle" :gutter="10" :wrap="false">
<a-col flex="none">
<a-typography-text>{{ $t('路段类型') }}:</a-typography-text>
</a-col>
<a-col flex="auto">
<a-select :value="route.type" @change="editor.changeRouteType(id, <MapRouteType>$event)">
<a-select-option v-for="[l, v] in MAP_ROUTE_TYPES" :key="v">{{ $t(l) }}</a-select-option>
</a-select>
</a-col>
</a-row>
<template v-if="MapRouteType.三阶贝塞尔曲线 === route.type">
<a-row align="middle" :gutter="8">
<a-col flex="auto">
<a-typography-text>{{ $t('控制点1') }}:</a-typography-text>
</a-col>
<a-col flex="none">
<a-space :size="8">
<a-typography-text code>X:</a-typography-text>
<a-input-number
style="width: 80px"
:placeholder="$t('请输入')"
:precision="0"
:controls="false"
:value="route?.c1?.x.toFixed()"
@change="editor.updateRoute(id, { c1: { x: <number>$event, y: route?.c1?.y ?? 0 } })"
/>
</a-space>
</a-col>
<a-col flex="none">
<a-space :size="8">
<a-typography-text code>Y:</a-typography-text>
<a-input-number
style="width: 80px"
:placeholder="$t('请输入')"
:precision="0"
:controls="false"
:value="route?.c1?.y.toFixed()"
@change="editor.updateRoute(id, { c1: { x: route?.c1?.x ?? 0, y: <number>$event } })"
/>
</a-space>
</a-col>
</a-row>
<a-row align="middle" :gutter="8">
<a-col flex="auto">
<a-typography-text>{{ $t('控制点2') }}:</a-typography-text>
</a-col>
<a-col flex="none">
<a-space :size="8">
<a-typography-text code>X:</a-typography-text>
<a-input-number
style="width: 80px"
:placeholder="$t('请输入')"
:precision="0"
:controls="false"
:value="route?.c2?.x.toFixed()"
@change="editor.updateRoute(id, { c2: { x: <number>$event, y: route?.c2?.y ?? 0 } })"
/>
</a-space>
</a-col>
<a-col flex="none">
<a-space :size="8">
<a-typography-text code>Y:</a-typography-text>
<a-input-number
style="width: 80px"
:placeholder="$t('请输入')"
:precision="0"
:controls="false"
:value="route?.c2?.y.toFixed()"
@change="editor.updateRoute(id, { c2: { x: route?.c2?.x ?? 0, y: <number>$event } })"
/>
</a-space>
</a-col>
</a-row>
</template>
</a-flex>
<a-empty v-else :image="sTheme.empty" />
</a-card>
</template>

View File

@ -47,6 +47,7 @@ const importScene = async () => {
const exportScene = () => {
const json = editor.value?.save();
if (!json) return;
getSceneById(json);
const blob = textToBlob(json);
if (!blob?.size) return;
const url = URL.createObjectURL(blob);
@ -142,7 +143,8 @@ const selectRobot = (id: string) => {
<PointDetailCard v-else :token="EDITOR_KEY" :current="current.id" />
</template>
<template v-if="isRoute">
<div v-if="editable"></div>
<RouteEditCard v-if="editable" :token="EDITOR_KEY" :id="current.id" />
<RouteDetailCard v-else :token="EDITOR_KEY" :current="current.id" />
</template>
<template v-if="isArea">
<AreaEditCard v-if="editable" :token="EDITOR_KEY" :id="current.id" />

View File

@ -5,6 +5,7 @@ import {
type MapPen,
type MapPointInfo,
MapPointType,
type MapRouteInfo,
MapRoutePassType,
MapRouteType,
type Point,
@ -35,7 +36,7 @@ export class EditorService extends Meta2d {
}
public setState(editable?: boolean): void {
this.lock(editable ? LockState.None : LockState.DisableMoveScale);
this.lock(editable ? LockState.None : LockState.DisableEdit);
}
public override data(): SceneData {
@ -245,7 +246,7 @@ export class EditorService extends Meta2d {
{ initialValue: new Array<MapPen>() },
);
public getRouteLabel(id?: string): string {
public getRouteLabel(id?: string, d?: number): string {
if (!id) return '';
const pen = this.getPenById(id);
if (isNil(pen)) return '';
@ -255,15 +256,15 @@ export class EditorService extends Meta2d {
const p2 = this.getPenById(a2.connectTo);
if (isNil(p1) || isNil(p2)) return '';
const { direction = 1 } = pen.route ?? {};
return `${p1.text} ${direction > 0 ? '→' : '←'} ${p2.text}`;
return `${p1.label}${(d ?? direction) > 0 ? '→' : '←'}${p2.label}`;
}
public addRoute(p: [string, string], type = MapRouteType.线): void {
const [p1, p2] = p.map((v) => this.getPenById(v));
if (!p1?.anchors?.length || !p2?.anchors?.length) return;
const line = this.connectLine(p1, p2, undefined, undefined, false);
const pen: MapPen = { tags: ['route'], route: { type }, lineWidth: 2 };
this.bottom(line);
const line = this.connectLine(p1, p2, p1.anchors[0], p2.anchors[0], false);
const pen: MapPen = { tags: ['route'], route: { type }, lineWidth: 2, iconSize: 10 };
// this.bottom(line);
this.setValue({ id: line.id, ...pen }, { render: false, history: false, doEvent: false });
this.updateLineType(line, type);
// this.pushHistory({ type: EditType.Add, pens: [cloneDeep(line)] });
@ -271,6 +272,12 @@ export class EditorService extends Meta2d {
this.render();
}
public updateRoute(id: string, info: Partial<MapRouteInfo>): void {
const { route } = this.getPenById(id) ?? {};
if (!route?.type) return;
const o = { ...route, ...info };
this.setValue({ id, route: o }, { render: true, history: true, doEvent: true });
}
public changeRouteType(id: string, type: MapRouteType): void {
const pen = this.getPenById(id);
if (isNil(pen)) return;
@ -377,6 +384,8 @@ export class EditorService extends Meta2d {
this.render();
}
#onDelete(): void {}
// eslint-disable-next-line @typescript-eslint/no-explicit-any
#listen(e: unknown, v: any) {
switch (e) {
@ -386,8 +395,15 @@ export class EditorService extends Meta2d {
break;
case 'add':
this.#change$$.next(true);
break;
case 'delete':
this.#onDelete();
this.#change$$.next(true);
break;
case 'update':
this.#change$$.next(true);
break;
case 'valueUpdate':
this.#change$$.next(true);
break;
@ -438,6 +454,7 @@ export class EditorService extends Meta2d {
}
#register() {
this.register({ line: () => new Path2D() });
this.registerCanvasDraw({ point: drawPoint, line: drawLine, area: drawArea });
this.registerAnchors({ point: anchorPoint });
this.addDrawLineFn('bezier3', lineBezier3);
@ -511,11 +528,17 @@ function drawPoint(ctx: CanvasRenderingContext2D, pen: MapPen): void {
ctx.restore();
}
function anchorPoint(pen: MapPen): void {
pen.anchors = [{ penId: pen.id, id: 'c', x: 0.5, y: 0.5 }];
pen.anchors = [
{ penId: pen.id, id: 't', x: 0.5, y: 0 },
{ penId: pen.id, id: 'b', x: 0.5, y: 1 },
{ penId: pen.id, id: 'l', x: 0, y: 0.5 },
{ penId: pen.id, id: 'r', x: 1, y: 0.5 },
];
}
function drawLine(ctx: CanvasRenderingContext2D, pen: MapPen): void {
const theme = sTheme.editor;
const { iconSize: s = 10 } = pen.calculative ?? {};
const [p1, p2] = pen.calculative?.worldAnchors ?? [];
const { x: x1 = 0, y: y1 = 0 } = p1 ?? {};
const { x: x2 = 0, y: y2 = 0 } = p2 ?? {};
@ -524,8 +547,9 @@ function drawLine(ctx: CanvasRenderingContext2D, pen: MapPen): void {
const { x: dx2 = 0, y: dy2 = y2 - y1 } = c2 ?? {};
ctx.save();
ctx.moveTo(x1, y1);
ctx.beginPath();
ctx.strokeStyle = get(theme, `route.stroke-${pass}`) ?? '';
ctx.moveTo(x1, y1);
switch (type) {
case MapRouteType.线:
ctx.lineTo(x2, y2);
@ -539,8 +563,32 @@ function drawLine(ctx: CanvasRenderingContext2D, pen: MapPen): void {
break;
}
if (pass === MapRoutePassType.) {
ctx.setLineDash([8, 4]);
ctx.setLineDash([s / 2]);
}
ctx.stroke();
ctx.beginPath();
ctx.setLineDash([0]);
let r = (() => {
switch (type) {
case MapRouteType.线:
return Math.atan2(y2 - y1, x2 - x1);
case MapRouteType.线:
return direction < 0 ? Math.atan2(dy1, dx1) : Math.atan2(dy2, dx2);
default:
return 0;
}
})();
if (direction < 0) {
ctx.translate(x1, y1);
} else {
ctx.translate(x2, y2);
r += Math.PI;
}
ctx.moveTo(Math.cos(r + Math.PI / 5) * s, Math.sin(r + Math.PI / 5) * s);
ctx.lineTo(0, 0);
ctx.lineTo(Math.cos(r - Math.PI / 5) * s, Math.sin(r - Math.PI / 5) * s);
ctx.stroke();
ctx.setTransform(1, 0, 0, 1, 0, 0);
ctx.restore();
}
function lineBezier3(_: Meta2dStore, pen: MapPen): void {