This commit is contained in:
chndfang 2025-05-09 00:17:02 +08:00
parent f90d025e5a
commit 081764b4b1
10 changed files with 126 additions and 16 deletions

File diff suppressed because one or more lines are too long

View File

@ -394,8 +394,20 @@
padding: 0;
border-color: get-color(item_bg-hover);
& > .ant-list-item-action {
visibility: hidden;
& > li {
padding: 0;
}
}
&:hover {
background-color: get-color(item_bg-hover);
& > .ant-list-item-action {
visibility: visible;
}
}
&.selected {

View File

@ -30,6 +30,8 @@ export interface MapRouteInfo {
type: MapRouteType; // 线路类型
direction?: -1 | 1; // 方向
pass?: MapRoutePassType; // 可通行类型
c1?: Point; // 控制点A
c2?: Point; // 控制点B
}
//#endregion
@ -41,4 +43,5 @@ export interface MapAreaInfo {
}
//#endregion
export type MapRect = Record<'x' | 'y' | 'width' | 'height', number>;
export type Point = Record<'x' | 'y', number>;
export type Rect = Record<'x' | 'y' | 'width' | 'height', number>;

View File

@ -24,6 +24,7 @@ $icons: (
redo,
register,
robot,
route,
save,
search,
trash_fill,

Binary file not shown.

After

Width:  |  Height:  |  Size: 812 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 767 B

After

Width:  |  Height:  |  Size: 859 B

View File

@ -13,6 +13,12 @@
"stroke": "#595959",
"strokeActive": "#FCC947"
},
"route": {
"stroke-0": "#8C8C8C",
"stroke-1": "#49AA19",
"stroke-2": "#D89614",
"stroke-10": "#E63A3A"
},
"area": {
"stroke-1": "#9ACDFF99",
"fill-1": "#9ACDFF33",
@ -21,6 +27,5 @@
"stroke-12": "#0DBB8A99",
"fill-12": "#0DBB8A33",
"strokeActive": "#FCC947"
},
"line": "#8C8C8C"
}
}

View File

@ -1,5 +1,5 @@
<script setup lang="ts">
import { MAP_POINT_TYPES, MapAreaType, type MapPen, type MapPointInfo, MapPointType, type MapRect } from '@api/map';
import { MAP_POINT_TYPES, MapAreaType, type MapPen, type MapPointInfo, MapPointType, type Rect } from '@api/map';
import type { RobotInfo } from '@api/robot';
import type { PointBindModalRef } from '@common/modal/point-bind-modal.vue';
import type { RobotBindModalRef } from '@common/modal/robot-bind-modal.vue';
@ -27,7 +27,7 @@ const point = computed<MapPointInfo | null>(() => {
return v;
});
const rect = computed<MapRect | null>(() => {
const rect = computed<Rect | null>(() => {
if (isNil(pen.value)) return null;
return editor.value.getPenRect(pen.value);
});

View File

@ -46,7 +46,7 @@ watch(
() => selected.clear(),
);
const isAllSelected = computed<boolean>(() => robots.value.every(({ id }) => selected.has(id)));
const isAllSelected = computed<boolean>(() => selected.size > 0 && robots.value.every(({ id }) => selected.has(id)));
const selectAll = (checked: boolean) => {
if (checked) {
robots.value.forEach(({ id }) => selected.add(id));
@ -90,6 +90,19 @@ const toDeleteGroup = (id: RobotGroup['id']) =>
//#endregion
//#region
const toRemoveRobot = (id: RobotInfo['id']) =>
Modal.confirm({
class: 'confirm',
title: t('您确定要从场景中移除该机器人吗?'),
centered: true,
cancelText: t('返回'),
okText: t('移除'),
onOk: () => {
editor.value.removeRobots([id]);
selected.delete(id);
},
});
const toRemoveRobots = () =>
Modal.confirm({
class: 'confirm',
@ -198,11 +211,20 @@ const toRemoveRobots = () =>
<a-list rowKey="id" :data-source="getGroupRobots(robots)">
<template #renderItem="{ item }">
<a-list-item
class="ph-16"
:class="{ selected: item.id === current }"
:class="{ 'ph-16': !editable, 'pl-12': editable, 'pr-8': editable, selected: item.id === current }"
style="height: 36px"
@click="emit('change', item.id)"
>
<template v-if="editable" #actions>
<a-button
class="icon-btn panel-btn"
:title="$t('移除机器人')"
size="small"
@click.stop="toRemoveRobot(item.id)"
>
<i class="icon trash_fill" />
</a-button>
</template>
<a-space align="center" :size="8">
<a-checkbox
v-if="editable"

View File

@ -1,20 +1,33 @@
import { EDITOR_CONFIG, type MapAreaInfo, MapAreaType, type MapPen, type MapPointInfo, MapPointType } from '@api/map';
import {
EDITOR_CONFIG,
type MapAreaInfo,
MapAreaType,
type MapPen,
type MapPointInfo,
MapPointType,
MapRoutePassType,
MapRouteType,
type Point,
} from '@api/map';
import type { RobotGroup, RobotInfo } from '@api/robot';
import type { SceneData } from '@api/scene';
import sTheme from '@core/theme.service';
import { CanvasLayer, EditType, LockState, Meta2d, type Pen, s8 } from '@meta2d/core';
import { CanvasLayer, EditType, LockState, Meta2d, type Meta2dStore, type Pen, s8 } from '@meta2d/core';
import { useObservable } from '@vueuse/rxjs';
import { clone, cloneDeep, get, isNil, isString, mapKeys, pick, remove, some } from 'lodash-es';
import { BehaviorSubject, debounceTime, filter, map, Subject, switchMap } from 'rxjs';
import { reactive, watch } from 'vue';
export type Point = Record<'x' | 'y', number>;
export class EditorService extends Meta2d {
public async load(map?: string, editable = false): Promise<void> {
const data = map ? JSON.parse(map) : undefined;
this.open(data);
this.setState(editable);
setTimeout(() => {
this.addRoute(['21b74c90', '00f32a0']);
this.addRoute(['21b74c90', '7f201a25'], 'bezier3');
}, 1000);
}
public save(): string {
const data = this.data();
@ -244,6 +257,26 @@ export class EditorService extends Meta2d {
const { direction = 1 } = pen.route ?? {};
return `${p1.text} ${direction > 0 ? '→' : '←'} ${p2.text}`;
}
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);
this.setValue({ id: line.id, ...pen }, { render: false, history: false, doEvent: false });
this.updateLineType(line, type);
// this.pushHistory({ type: EditType.Add, pens: [cloneDeep(line)] });
this.active([line]);
this.render();
}
public changeRouteType(id: string, type: MapRouteType): void {
const pen = this.getPenById(id);
if (isNil(pen)) return;
this.updateLineType(pen, type);
this.setValue({ id, route: { type } }, { render: true, history: true, doEvent: true });
}
//#endregion
//#region 区域
@ -407,6 +440,7 @@ export class EditorService extends Meta2d {
#register() {
this.registerCanvasDraw({ point: drawPoint, line: drawLine, area: drawArea });
this.registerAnchors({ point: anchorPoint });
this.addDrawLineFn('bezier3', lineBezier3);
}
}
@ -477,16 +511,49 @@ function drawPoint(ctx: CanvasRenderingContext2D, pen: MapPen): void {
ctx.restore();
}
function anchorPoint(pen: MapPen): void {
pen.anchors = [{ x: 0.5, y: 0.5 }];
pen.anchors = [{ penId: pen.id, id: 'c', x: 0.5, y: 0.5 }];
}
function drawLine(ctx: CanvasRenderingContext2D, pen: MapPen): void {
const theme = sTheme.editor;
const [p1, p2] = pen.calculative?.worldAnchors ?? [];
const { direction } = pen.route ?? {};
const { x: x1 = 0, y: y1 = 0 } = p1 ?? {};
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 ?? {};
ctx.save();
ctx.lineWidth = 2;
ctx.moveTo(x1, y1);
ctx.strokeStyle = get(theme, `route.stroke-${pass}`) ?? '';
switch (type) {
case MapRouteType.线:
ctx.lineTo(x2, y2);
break;
case MapRouteType.线:
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 };
break;
default:
break;
}
if (pass === MapRoutePassType.) {
ctx.setLineDash([8, 4]);
}
ctx.restore();
}
function lineBezier3(_: Meta2dStore, pen: MapPen): void {
if (pen.calculative?.worldAnchors?.length !== 2) return;
const { c1, c2 } = 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: dx1 = x2 - x1, y: dy1 = 0 } = c1 ?? {};
const { x: dx2 = 0, y: dy2 = y2 - y1 } = c2 ?? {};
pen.calculative.worldAnchors[0].next = { x: x1 + dx1, y: y1 + dy1 };
pen.calculative.worldAnchors[1].prev = { x: x2 - dx2, y: y2 - dy2 };
}
function drawArea(ctx: CanvasRenderingContext2D, pen: MapPen): void {
const theme = sTheme.editor;