From 56f365bd0440dcdbe0f63e52b19187753fe9f286 Mon Sep 17 00:00:00 2001 From: xudan Date: Wed, 15 Oct 2025 15:19:14 +0800 Subject: [PATCH] =?UTF-8?q?refactor:=20=E7=A7=BB=E9=99=A4=20EditorRobotCon?= =?UTF-8?q?text=20=E6=8E=A5=E5=8F=A3=E4=B8=AD=E7=9A=84=20ensureCorrectLaye?= =?UTF-8?q?rOrder=20=E6=96=B9=E6=B3=95=EF=BC=8C=E5=B9=B6=E5=9C=A8=20Editor?= =?UTF-8?q?RobotService=20=E4=B8=AD=E5=88=A0=E9=99=A4=E7=9B=B8=E5=85=B3?= =?UTF-8?q?=E8=B0=83=E7=94=A8=EF=BC=8C=E7=AE=80=E5=8C=96=E4=BB=A3=E7=A0=81?= =?UTF-8?q?=E7=BB=93=E6=9E=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/services/editor.service.ts | 360 +++--------------- src/services/editor/area-manager.service.ts | 90 +++++ .../{ => editor}/editor-robot.service.ts | 4 +- src/services/editor/point-manager.service.ts | 117 ++++++ src/services/editor/route-manager.service.ts | 135 +++++++ 5 files changed, 386 insertions(+), 320 deletions(-) create mode 100644 src/services/editor/area-manager.service.ts rename src/services/{ => editor}/editor-robot.service.ts (99%) create mode 100644 src/services/editor/point-manager.service.ts create mode 100644 src/services/editor/route-manager.service.ts diff --git a/src/services/editor.service.ts b/src/services/editor.service.ts index 0c5b1c6..d1093ec 100644 --- a/src/services/editor.service.ts +++ b/src/services/editor.service.ts @@ -21,7 +21,7 @@ import type { StandardSceneRoute, } from '@api/scene'; import sTheme from '@core/theme.service'; -import { CanvasLayer, LockState, Meta2d, type Meta2dStore, type Pen, s8 } from '@meta2d/core'; +import { LockState, Meta2d, type Meta2dStore, type Pen } from '@meta2d/core'; import { useObservable } from '@vueuse/rxjs'; import { clone, get, isEmpty, isNil, isString, pick, } from 'lodash-es'; import { debounceTime, filter, map, Subject, switchMap } from 'rxjs'; @@ -36,7 +36,10 @@ import { drawStorageLocation, drawStorageMore, } from './draw/storage-location-drawer'; -import { drawRobot,EditorRobotService } from './editor-robot.service'; +import { AreaManager } from './editor/area-manager.service'; +import { drawRobot,EditorRobotService } from './editor/editor-robot.service'; +import { PointManager } from './editor/point-manager.service'; +import { RouteManager } from './editor/route-manager.service'; import { LayerManagerService } from './layer-manager.service'; import { createStorageLocationUpdater,StorageLocationService } from './storage-location.service'; import { AutoStorageGenerator } from './utils/auto-storage-generator'; @@ -65,6 +68,13 @@ export class EditorService extends Meta2d { /** 自动生成库位工具实例 */ private readonly autoStorageGenerator!: AutoStorageGenerator; + /** 点位管理模块 */ + private readonly pointManager: PointManager; + /** 路线管理模块 */ + private readonly routeManager: RouteManager; + /** 区域管理模块 */ + private readonly areaManager: AreaManager; + //#region 场景文件管理 /** * 加载场景文件到编辑器 @@ -1048,9 +1058,7 @@ export class EditorService extends Meta2d { ); public getPointRect(pen?: MapPen): Rect | null { - if (isNil(pen)) return null; - const { x, y, width, height } = this.getPenRect(pen); - return { x: x + width / 2, y: y + height / 2, width, height }; + return this.pointManager.getPointRect(pen); } /** @@ -1060,99 +1068,18 @@ export class EditorService extends Meta2d { * @param id 点位ID,未指定则自动生成 */ public async addPoint(p: Point, type = MapPointType.普通点, id?: string): Promise { - id ||= s8(); - const pointInfo: MapPointInfo = { type }; - - // 为充电点/停靠点设置默认启用状态 - if ([MapPointType.充电点, MapPointType.停靠点].includes(type)) { - pointInfo.enabled = 1; - } - - const pen: MapPen = { - ...p, - ...this.#mapPoint(type), - ...this.#mapPointImage(type), - id, - name: 'point', - tags: ['point'], - label: `P${id}`, - point: pointInfo, - locked: LockState.DisableEdit, - }; - pen.x! -= pen.width! / 2; - pen.y! -= pen.height! / 2; - const addedPen = await this.addPen(pen, false, true, true); - - // 将新创建的点位移到最上层 - this.layerManager.adjustElementLayer(addedPen); - - // 如果是动作点且有库位信息,创建库位pen对象 - if (type === MapPointType.动作点 && pointInfo.associatedStorageLocations?.length) { - this.createStorageLocationPens(addedPen.id!, pointInfo.associatedStorageLocations); - } + await this.pointManager.addPoint(p, type, id); } public updatePoint(id: string, info: Partial, autoCreateStorage = true): void { - const { point } = this.getPenById(id) ?? {}; - if (!point?.type) return; - const o = { ...point, ...info }; - this.setValue({ id, point: o }, { render: true, history: true, doEvent: true }); - - // 如果是动作点且库位信息发生变化,重新创建库位pen对象 - if (point.type === MapPointType.动作点 && info.associatedStorageLocations && autoCreateStorage) { - this.createStorageLocationPens(id, info.associatedStorageLocations); - } + this.pointManager.updatePoint(id, info, autoCreateStorage); } + public changePointType(id: string, type: MapPointType): void { - const pen = this.getPenById(id); - const rect = this.getPointRect(pen); - if (isNil(rect)) return; - - // 如果原点位是动作点,需要先删除相关的库位元素 - if (pen?.point?.type === MapPointType.动作点) { - this.removeStorageLocationPens(id); - } - - const point = this.#mapPoint(type); - const pointInfo: MapPointInfo = { type }; - - // 为充电点/停靠点设置默认启用状态 - if ([MapPointType.充电点, MapPointType.停靠点].includes(type)) { - pointInfo.enabled = 1; - } - - this.setValue( - { - id, - x: rect.x - point.width / 2, - y: rect.y - point.height / 2, - ...point, - ...this.#mapPointImage(type), - point: pointInfo, - }, - { render: true, history: true, doEvent: true }, - ); - - // 如果是大点位类型(需要图片),异步重新加载主题确保图片正确显示 - if (type >= 10) { - requestAnimationFrame(() => { - this.reloadTheme(); - }); - } + this.pointManager.changePointType(id, type); } - #mapPoint(type: MapPointType): Required> { - const width = type < 10 ? 24 : 48; - const height = type < 10 ? 24 : 60; - const lineWidth = type < 10 ? 2 : 3; - const iconSize = type < 10 ? 4 : 10; - return { width, height, lineWidth, iconSize }; - } - #mapPointImage(type: MapPointType): Required> { - const theme = this.data().theme; - const image = type < 10 ? '' : `${import.meta.env.BASE_URL}/point/${type}-${theme}.png`; - return { image, canvasLayer: CanvasLayer.CanvasMain }; - } + //#endregion @@ -1167,181 +1094,47 @@ export class EditorService extends Meta2d { { initialValue: new Array() }, ); - public getRouteLabel(id?: string, d?: number): 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.label}${(d ?? direction) > 0 ? '→' : '←'}${p2.label}`; + public getRouteLabel(id?: string, directionOverride?: number): string { + return this.routeManager.getRouteLabel(id, directionOverride); } - /** - * 在两个点位之间添加路线 - * @param p 两个点位的数组 - * @param type 路线类型,默认为直线 - * @param id 路线ID,未指定则自动生成 - */ public addRoute(p: [MapPen, MapPen], type = MapRouteType.直线, id?: string): void { - const [p1, p2] = p; - if (!p1?.anchors?.length || !p2?.anchors?.length) return; - const line = this.connectLine(p1, p2, undefined, undefined, false); - id ||= line.id!; - this.changePenId(line.id!, id); - const pen: MapPen = { - tags: ['route'], - route: { type }, - lineWidth: 1, - locked: LockState.DisableEdit, - canvasLayer: CanvasLayer.CanvasMain, - }; - this.setValue({ id, ...pen }, { render: false, history: false, doEvent: false }); - this.updateLineType(line, type); - - // 将路线移到底层,确保点位能覆盖在路线之上 - this.bottom([line]); - - this.active(id); - this.render(); + this.routeManager.addRoute(p, type, id); } public updateRoute(id: string, info: Partial): 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 }); + this.routeManager.updateRoute(id, info); } + 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 }); + this.routeManager.changeRouteType(id, type); } - /** - * 获取两个点位之间的所有路线 - * @param point1Id 第一个点位ID - * @param point2Id 第二个点位ID - * @returns 连接这两个点位的所有路线数组 - */ public getRoutesBetweenPoints(point1Id: string, point2Id: string): MapPen[] { - return this.find('route').filter(route => { - const [a1, a2] = route.anchors ?? []; - if (!a1?.connectTo || !a2?.connectTo) return false; - return (a1.connectTo === point1Id && a2.connectTo === point2Id) || - (a1.connectTo === point2Id && a2.connectTo === point1Id); - }); + return this.routeManager.getRoutesBetweenPoints(point1Id, point2Id); } - /** - * 获取指定路线的反向路线 - * @param routeId 路线ID - * @returns 反向路线,如果不存在则返回null - */ public getReverseRoute(routeId: string): MapPen | null { - const route = this.getPenById(routeId); - if (!route) return null; - - const [a1, a2] = route.anchors ?? []; - if (!a1?.connectTo || !a2?.connectTo) return null; - - const reverseRoutes = this.getRoutesBetweenPoints(a1.connectTo, a2.connectTo); - return reverseRoutes.find(r => r.id !== routeId) || null; + return this.routeManager.getReverseRoute(routeId); } - /** - * 创建双向路线 - * @param p 两个点位的数组 - * @param type 路线类型,默认为直线 - * @param forwardId 正向路线ID,未指定则自动生成 - * @param reverseId 反向路线ID,未指定则自动生成 - */ public addBidirectionalRoute( - p: [MapPen, MapPen], - type = MapRouteType.直线, - forwardId?: string, - reverseId?: string + p: [MapPen, MapPen], + type = MapRouteType.直线, + forwardId?: string, + reverseId?: string, ): void { - const [p1, p2] = p; - if (!p1?.anchors?.length || !p2?.anchors?.length) return; - - // 创建正向路线 - const forwardLine = this.connectLine(p1, p2, undefined, undefined, false); - forwardId ||= forwardLine.id!; - this.changePenId(forwardLine.id!, forwardId); - const forwardPen: MapPen = { - tags: ['route'], - route: { type, direction: 1 }, - lineWidth: 1, - locked: LockState.DisableEdit, - canvasLayer: CanvasLayer.CanvasMain, - }; - this.setValue({ id: forwardId, ...forwardPen }, { render: false, history: false, doEvent: false }); - this.updateLineType(forwardLine, type); - - // 创建反向路线 - const reverseLine = this.connectLine(p2, p1, undefined, undefined, false); - reverseId ||= reverseLine.id!; - this.changePenId(reverseLine.id!, reverseId); - const reversePen: MapPen = { - tags: ['route'], - route: { type, direction: -1 }, - lineWidth: 1, - locked: LockState.DisableEdit, - canvasLayer: CanvasLayer.CanvasMain, - }; - this.setValue({ id: reverseId, ...reversePen }, { render: false, history: false, doEvent: false }); - this.updateLineType(reverseLine, type); - - // 将路线移到底层,确保点位能覆盖在路线之上 - this.bottom([forwardLine, reverseLine]); - - // 将反向路线移到正向路线之上,解决重叠选择问题 - this.top([reverseLine]); - - this.active(forwardId); - this.render(); + this.routeManager.addBidirectionalRoute(p, type, forwardId, reverseId); } - /** - * 删除双向路线 - * @param routeId 任意一条路线的ID - */ public removeBidirectionalRoute(routeId: string): void { - const route = this.getPenById(routeId); - if (!route) return; - - const [a1, a2] = route.anchors ?? []; - if (!a1?.connectTo || !a2?.connectTo) return; - - // 找到所有连接这两个点位的路线 - const routesBetweenPoints = this.getRoutesBetweenPoints(a1.connectTo, a2.connectTo); - - // 删除所有相关路线 - routesBetweenPoints.forEach(r => { - this.delete([r]); - }); - - this.render(); + this.routeManager.removeBidirectionalRoute(routeId); } - /** - * 检查两个点位之间是否存在双向路线 - * @param point1Id 第一个点位ID - * @param point2Id 第二个点位ID - * @returns 是否存在双向路线 - */ public hasBidirectionalRoute(point1Id: string, point2Id: string): boolean { - const routes = this.getRoutesBetweenPoints(point1Id, point2Id); - return routes.length >= 2; + return this.routeManager.hasBidirectionalRoute(point1Id, point2Id); } - //#endregion - //#region 区域 /** 画布上所有区域对象列表,响应式更新 */ public readonly areas = useObservable( @@ -1354,84 +1147,17 @@ export class EditorService extends Meta2d { ); public getBoundAreas(id: string = '', name: 'point' | 'line', type: MapAreaType): MapPen[] { - if (!id) return []; - return this.find(`area-${type}`).filter(({ area }) => { - if (name === 'point') return area?.points?.includes(id); - if (name === 'line') return area?.routes?.includes(id); - return false; - }); + return this.areaManager.getBoundAreas(id, name, type); } - /** - * 在指定区域添加功能区域 - * @param p1 区域起始坐标 - * @param p2 区域结束坐标 - * @param type 区域类型,默认为库区 - * @param id 区域ID,未指定则自动生成 - */ - public async addArea(p1: Point, p2: Point, type = MapAreaType.库区, id?: string) { - const w = Math.abs(p1.x - p2.x); - const h = Math.abs(p1.y - p2.y); - if (w < 50 || h < 60) return; - const points = new Array(); - const routes = new Array(); - if (!id) { - id = s8(); - const selected = this.store.active; - 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!)); - // 互斥区不再绑定路段 - break; - case MapAreaType.非互斥区: - selected?.filter(({ point }) => point?.type).forEach(({ id }) => points.push(id!)); - break; - case MapAreaType.约束区: - selected?.filter(({ point }) => point?.type).forEach(({ id }) => points.push(id!)); - break; - case MapAreaType.描述区: - // 描述区不需要绑定点位或路线 - break; - - default: - break; - } - } - const areaInfo: MapAreaInfo = { type, points, routes }; - if (type === MapAreaType.库区) { - areaInfo.inoutflag = 2; - } - 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, - lineWidth: 1, - area: areaInfo, - locked: LockState.None, - }; - const area = await this.addPen(pen, true, true, true); - this.bottom(area); - - // 如果是库区且包含动作点,触发自动生成库位的确认对话框 - if (type === MapAreaType.库区 && points.length > 0) { - this.autoStorageGenerator.triggerAutoCreateStorageDialog(pen, points); - } + public async addArea(p1: Point, p2: Point, type = MapAreaType.库区, id?: string): Promise { + await this.areaManager.addArea(p1, p2, type, id); } public updateArea(id: string, info: Partial): void { - const { area } = this.getPenById(id) ?? {}; - if (!area?.type) return; - const o = { ...area, ...info }; - this.setValue({ id, area: o }, { render: true, history: true, doEvent: true }); + this.areaManager.updateArea(id, info); } + //#endregion /** @@ -1458,6 +1184,10 @@ export class EditorService extends Meta2d { // 初始化BinTask管理服务 this.binTaskManager = new BinTaskManagerService(this); + this.pointManager = new PointManager(this, this.layerManager); + this.routeManager = new RouteManager(this); + this.areaManager = new AreaManager(this, this.autoStorageGenerator); + // 设置颜色配置服务的编辑器实例 colorConfig.setEditorService(this); @@ -1499,11 +1229,7 @@ export class EditorService extends Meta2d { this.setTheme(theme); this.setOptions({ color: get(sTheme.editor, 'color') }); - this.find('point').forEach((pen) => { - if (!pen.point?.type) return; - if (pen.point.type < 10) return; - this.canvas.updateValue(pen, this.#mapPointImage(pen.point.type)); - }); + this.pointManager.refreshPointImages(); this.find('robot').forEach((pen) => { if (!pen.robot?.type) return; // 从pen的text属性获取机器人名称 diff --git a/src/services/editor/area-manager.service.ts b/src/services/editor/area-manager.service.ts new file mode 100644 index 0000000..7622411 --- /dev/null +++ b/src/services/editor/area-manager.service.ts @@ -0,0 +1,90 @@ +import { + type MapAreaInfo, + MapAreaType, + type MapPen, + MapPointType, + type Point, +} from '@api/map'; +import { LockState, s8 } from '@meta2d/core'; + +import type { EditorService } from '../editor.service'; +import type { AutoStorageGenerator } from '../utils/auto-storage-generator'; + +export class AreaManager { + constructor( + private readonly editor: EditorService, + private readonly autoStorageGenerator: AutoStorageGenerator, + ) {} + + public getBoundAreas(id: string, name: 'point' | 'line', type: MapAreaType): MapPen[] { + if (!id) return []; + return this.editor.find(`area-${type}`).filter(({ area }) => { + if (name === 'point') return area?.points?.includes(id); + if (name === 'line') return area?.routes?.includes(id); + return false; + }); + } + + public async addArea(p1: Point, p2: Point, type = MapAreaType.库区, id?: string): Promise { + const w = Math.abs(p1.x - p2.x); + const h = Math.abs(p1.y - p2.y); + if (w < 50 || h < 60) return; + + const points: string[] = []; + const routes: string[] = []; + + if (!id) { + id = s8(); + const selected = this.editor.store.active as MapPen[] | undefined; + switch (type) { + case MapAreaType.库区: + selected + ?.filter(({ point }) => point?.type === MapPointType.动作点) + .forEach(({ id: pointId }) => points.push(pointId!)); + break; + case MapAreaType.互斥区: + case MapAreaType.非互斥区: + case MapAreaType.约束区: + selected?.filter(({ point }) => point?.type).forEach(({ id: pointId }) => points.push(pointId!)); + break; + case MapAreaType.描述区: + break; + default: + break; + } + } + + const areaInfo: MapAreaInfo = { type, points, routes }; + if (type === MapAreaType.库区) { + areaInfo.inoutflag = 2; + } + + 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, + lineWidth: 1, + area: areaInfo, + locked: LockState.None, + }; + + const area = await this.editor.addPen(pen, true, true, true); + this.editor.bottom(area); + + if (type === MapAreaType.库区 && points.length > 0) { + this.autoStorageGenerator.triggerAutoCreateStorageDialog(pen, points); + } + } + + public updateArea(id: string, info: Partial): void { + const { area } = this.editor.getPenById(id) ?? {}; + if (!area?.type) return; + const mergedArea = { ...area, ...info }; + this.editor.setValue({ id, area: mergedArea }, { render: true, history: true, doEvent: true }); + } +} diff --git a/src/services/editor-robot.service.ts b/src/services/editor/editor-robot.service.ts similarity index 99% rename from src/services/editor-robot.service.ts rename to src/services/editor/editor-robot.service.ts index 766a36e..09e387e 100644 --- a/src/services/editor-robot.service.ts +++ b/src/services/editor/editor-robot.service.ts @@ -11,7 +11,7 @@ import { reactive } from 'vue'; import cargoIcon from '../assets/icons/png/cargo.png'; import chargingIcon from '../assets/icons/png/charging.png'; import notAcceptingOrdersIcon from '../assets/icons/png/notAcceptingOrders.png'; -import colorConfig from './color/color-config.service'; +import colorConfig from '../color/color-config.service'; interface RobotStatusPosition { x: number; @@ -31,7 +31,6 @@ export interface EditorRobotContext { getPenRect(pen: MapPen): Rect; find(target: string): MapPen[]; inactive(): void; - ensureCorrectLayerOrder(): void; } export class EditorRobotService { @@ -276,7 +275,6 @@ export class EditorRobotService { }), ); - this.ctx.ensureCorrectLayerOrder(); } public updateAllRobotImageSizes(): void { diff --git a/src/services/editor/point-manager.service.ts b/src/services/editor/point-manager.service.ts new file mode 100644 index 0000000..7676a23 --- /dev/null +++ b/src/services/editor/point-manager.service.ts @@ -0,0 +1,117 @@ +import { type MapPen, type MapPointInfo, MapPointType, type Point, type Rect } from '@api/map'; +import { CanvasLayer, LockState, s8 } from '@meta2d/core'; +import { isNil } from 'lodash-es'; + +import type { EditorService } from '../editor.service'; +import type { LayerManagerService } from '../layer-manager.service'; + +export class PointManager { + constructor( + private readonly editor: EditorService, + private readonly layerManager: LayerManagerService, + ) {} + + public getPointRect(pen?: MapPen): Rect | null { + if (isNil(pen)) return null; + const { x, y, width, height } = this.editor.getPenRect(pen); + return { x: x + width / 2, y: y + height / 2, width, height }; + } + + public async addPoint(p: Point, type = MapPointType.普通点, id?: string): Promise { + id ||= s8(); + const pointInfo: MapPointInfo = { type }; + + if ([MapPointType.充电点, MapPointType.停靠点].includes(type)) { + pointInfo.enabled = 1; + } + + const pen: MapPen = { + ...p, + ...this.mapPoint(type), + ...this.mapPointImage(type), + id, + name: 'point', + tags: ['point'], + label: `P${id}`, + point: pointInfo, + locked: LockState.DisableEdit, + }; + pen.x! -= pen.width! / 2; + pen.y! -= pen.height! / 2; + const addedPen = await this.editor.addPen(pen, false, true, true); + + this.layerManager.adjustElementLayer(addedPen); + + if (type === MapPointType.动作点 && pointInfo.associatedStorageLocations?.length) { + this.editor.createStorageLocationPens(addedPen.id!, pointInfo.associatedStorageLocations); + } + } + + public updatePoint(id: string, info: Partial, autoCreateStorage = true): void { + const { point } = this.editor.getPenById(id) ?? {}; + if (!point?.type) return; + const mergedPoint = { ...point, ...info }; + this.editor.setValue({ id, point: mergedPoint }, { render: true, history: true, doEvent: true }); + + if (point.type === MapPointType.动作点 && info.associatedStorageLocations && autoCreateStorage) { + this.editor.createStorageLocationPens(id, info.associatedStorageLocations); + } + } + + public changePointType(id: string, type: MapPointType): void { + const pen = this.editor.getPenById(id); + const rect = this.getPointRect(pen); + if (isNil(rect)) return; + + if (pen?.point?.type === MapPointType.动作点) { + this.editor.removeStorageLocationPens(id); + } + + const point = this.mapPoint(type); + const pointInfo: MapPointInfo = { type }; + + if ([MapPointType.充电点, MapPointType.停靠点].includes(type)) { + pointInfo.enabled = 1; + } + + this.editor.setValue( + { + id, + x: rect.x - point.width / 2, + y: rect.y - point.height / 2, + ...point, + ...this.mapPointImage(type), + point: pointInfo, + }, + { render: true, history: true, doEvent: true }, + ); + + if (type >= 10) { + requestAnimationFrame(() => { + this.editor.reloadTheme(); + }); + } + } + + public refreshPointImages(): void { + this.editor.find('point').forEach((pen) => { + const type = pen.point?.type; + if (!type || type < 10) return; + this.editor.canvas.updateValue(pen, this.mapPointImage(type)); + }); + } + + private mapPoint(type: MapPointType): Required> { + const width = type < 10 ? 24 : 48; + const height = type < 10 ? 24 : 60; + const lineWidth = type < 10 ? 2 : 3; + const iconSize = type < 10 ? 4 : 10; + return { width, height, lineWidth, iconSize }; + } + + private mapPointImage(type: MapPointType): Required> { + const theme = this.editor.data().theme; + const image = type < 10 ? '' : `${import.meta.env.BASE_URL}/point/${type}-${theme}.png`; + return { image, canvasLayer: CanvasLayer.CanvasMain }; + } +} diff --git a/src/services/editor/route-manager.service.ts b/src/services/editor/route-manager.service.ts new file mode 100644 index 0000000..feae316 --- /dev/null +++ b/src/services/editor/route-manager.service.ts @@ -0,0 +1,135 @@ +import { type MapPen, type MapRouteInfo, MapRouteType } from '@api/map'; +import { CanvasLayer, LockState } from '@meta2d/core'; + +import type { EditorService } from '../editor.service'; + +export class RouteManager { + constructor(private readonly editor: EditorService) {} + + public getRouteLabel(id?: string, overrideDirection?: number): string { + if (!id) return ''; + const pen = this.editor.getPenById(id); + if (!pen) return ''; + const [a1, a2] = pen.anchors ?? []; + if (!a1?.connectTo || !a2?.connectTo) return ''; + const p1 = this.editor.getPenById(a1.connectTo); + const p2 = this.editor.getPenById(a2.connectTo); + if (!p1 || !p2) return ''; + const { direction = 1 } = pen.route ?? {}; + return `${p1.label}${(overrideDirection ?? direction) > 0 ? '→' : '←'}${p2.label}`; + } + + public addRoute(p: [MapPen, MapPen], type = MapRouteType.直线, id?: string): void { + const [p1, p2] = p; + if (!p1?.anchors?.length || !p2?.anchors?.length) return; + const line = this.editor.connectLine(p1, p2, undefined, undefined, false); + id ||= line.id!; + this.editor.changePenId(line.id!, id); + const pen: MapPen = { + tags: ['route'], + route: { type }, + lineWidth: 1, + locked: LockState.DisableEdit, + canvasLayer: CanvasLayer.CanvasMain, + }; + this.editor.setValue({ id, ...pen }, { render: false, history: false, doEvent: false }); + this.editor.updateLineType(line, type); + this.editor.bottom([line]); + + this.editor.active(id); + this.editor.render(); + } + + public updateRoute(id: string, info: Partial): void { + const { route } = this.editor.getPenById(id) ?? {}; + if (!route?.type) return; + const mergedRoute = { ...route, ...info }; + this.editor.setValue({ id, route: mergedRoute }, { render: true, history: true, doEvent: true }); + } + + public changeRouteType(id: string, type: MapRouteType): void { + const pen = this.editor.getPenById(id); + if (!pen) return; + this.editor.updateLineType(pen, type); + this.editor.setValue({ id, route: { type } }, { render: true, history: true, doEvent: true }); + } + + public getRoutesBetweenPoints(point1Id: string, point2Id: string): MapPen[] { + return this.editor.find('route').filter((route) => { + const [a1, a2] = route.anchors ?? []; + if (!a1?.connectTo || !a2?.connectTo) return false; + return ( + (a1.connectTo === point1Id && a2.connectTo === point2Id) || + (a1.connectTo === point2Id && a2.connectTo === point1Id) + ); + }); + } + + public getReverseRoute(routeId: string): MapPen | null { + const route = this.editor.getPenById(routeId); + if (!route) return null; + const [a1, a2] = route.anchors ?? []; + if (!a1?.connectTo || !a2?.connectTo) return null; + const reverseRoutes = this.getRoutesBetweenPoints(a1.connectTo, a2.connectTo); + return reverseRoutes.find((r) => r.id !== routeId) || null; + } + + public addBidirectionalRoute( + p: [MapPen, MapPen], + type = MapRouteType.直线, + forwardId?: string, + reverseId?: string, + ): void { + const [p1, p2] = p; + if (!p1?.anchors?.length || !p2?.anchors?.length) return; + + const forwardLine = this.editor.connectLine(p1, p2, undefined, undefined, false); + forwardId ||= forwardLine.id!; + this.editor.changePenId(forwardLine.id!, forwardId); + const forwardPen: MapPen = { + tags: ['route'], + route: { type, direction: 1 }, + lineWidth: 1, + locked: LockState.DisableEdit, + canvasLayer: CanvasLayer.CanvasMain, + }; + this.editor.setValue({ id: forwardId, ...forwardPen }, { render: false, history: false, doEvent: false }); + this.editor.updateLineType(forwardLine, type); + + const reverseLine = this.editor.connectLine(p2, p1, undefined, undefined, false); + reverseId ||= reverseLine.id!; + this.editor.changePenId(reverseLine.id!, reverseId); + const reversePen: MapPen = { + tags: ['route'], + route: { type, direction: -1 }, + lineWidth: 1, + locked: LockState.DisableEdit, + canvasLayer: CanvasLayer.CanvasMain, + }; + this.editor.setValue({ id: reverseId, ...reversePen }, { render: false, history: false, doEvent: false }); + this.editor.updateLineType(reverseLine, type); + + this.editor.bottom([forwardLine, reverseLine]); + this.editor.top([reverseLine]); + + this.editor.active(forwardId); + this.editor.render(); + } + + public removeBidirectionalRoute(routeId: string): void { + const route = this.editor.getPenById(routeId); + if (!route) return; + const [a1, a2] = route.anchors ?? []; + if (!a1?.connectTo || !a2?.connectTo) return; + const routesBetweenPoints = this.getRoutesBetweenPoints(a1.connectTo, a2.connectTo); + routesBetweenPoints.forEach((r) => { + this.editor.delete([r]); + }); + this.editor.render(); + } + + public hasBidirectionalRoute(point1Id: string, point2Id: string): boolean { + const routes = this.getRoutesBetweenPoints(point1Id, point2Id); + return routes.length >= 2; + } +}