diff --git a/src/services/area-operation.service.ts b/src/services/area-operation.service.ts new file mode 100644 index 0000000..d5e9316 --- /dev/null +++ b/src/services/area-operation.service.ts @@ -0,0 +1,186 @@ +import type { MapAreaInfo, MapPen } from '@api/map'; +import { MapAreaType, MapPointType } from '@api/map'; + +/** + * 区域操作服务类 + * 负责处理区域大小变化时的点位绑定检查和管理 + */ +export class AreaOperationService { + /** + * 处理区域大小变化事件 + * 当互斥区或非互斥区的大小发生变化时,自动检查并绑定新包含的点位 + * @param pen 发生变化的图形对象 + * @param findPoints 查找点位的函数 + * @param updateArea 更新区域的函数 + */ + public handleAreaSizeChange( + pen: MapPen, + findPoints: (type: string) => MapPen[], + updateArea: (id: string, info: Partial) => void, + ): void { + // 检查是否为区域类型 + if (!pen.area?.type || !['area'].includes(pen.tags?.[0] || '')) { + return; + } + + const areaType = pen.area.type; + + // 只处理互斥区和非互斥区 + if (![MapAreaType.互斥区, MapAreaType.非互斥区].includes(areaType)) { + return; + } + + // 获取区域边界 + const areaRect = this.getPenRect(pen); + if (!areaRect) { + return; + } + + // 查找所有点位 + const allPoints = findPoints('point') as MapPen[]; + + // 根据区域类型过滤点位,借鉴 addArea 的逻辑 + const containedPoints: string[] = []; + const currentBoundPoints = pen.area.points || []; + + allPoints.forEach((point) => { + if (!point.point?.type) return; + + // 根据区域类型过滤点位类型 + let shouldInclude = false; + switch (areaType) { + case MapAreaType.互斥区: + case MapAreaType.非互斥区: + // 互斥区和非互斥区绑定所有类型的点位 + shouldInclude = true; + break; + case MapAreaType.库区: + // 库区只绑定动作点 + shouldInclude = point.point.type === MapPointType.动作点; + break; + case MapAreaType.约束区: + // 约束区绑定所有类型的点位 + shouldInclude = true; + break; + default: + shouldInclude = false; + break; + } + + if (!shouldInclude) return; + + // 检查点位是否在区域内(使用点位中心坐标) + const pointCenter = this.getPointRect(point); + if (!pointCenter) return; + + const isContained = this.isPointInArea(pointCenter, areaRect); + + if (isContained) { + containedPoints.push(point.id!); + } + }); + + // 找出新包含的点位(之前未绑定的) + const newPoints = containedPoints.filter((id) => !currentBoundPoints.includes(id)); + + // 找出需要解绑的点位(之前绑定但现在不在区域内的) + const removedPoints = currentBoundPoints.filter((id) => !containedPoints.includes(id)); + + if (newPoints.length > 0 || removedPoints.length > 0) { + // 更新区域绑定的点位:保留仍在区域内的点位,添加新包含的点位 + const updatedPoints = containedPoints; + + updateArea(pen.id!, { points: updatedPoints }); + } + } + + /** + * 检查点位是否在指定区域内 + * @param point 点位坐标 + * @param area 区域边界 + * @returns 是否在区域内 + */ + private isPointInArea( + point: { x: number; y: number }, + area: { x: number; y: number; width: number; height: number }, + ): boolean { + return point.x >= area.x && point.x <= area.x + area.width && point.y >= area.y && point.y <= area.y + area.height; + } + + /** + * 获取区域边界矩形 + * @param pen 区域图形对象 + * @returns 区域边界矩形 + */ + private getPenRect(pen: MapPen): { x: number; y: number; width: number; height: number } | null { + if (!pen.x || !pen.y || !pen.width || !pen.height) { + return null; + } + return { + x: pen.x, + y: pen.y, + width: pen.width, + height: pen.height, + }; + } + + /** + * 获取点位中心坐标矩形 + * @param pen 点位图形对象 + * @returns 点位中心坐标矩形 + */ + private getPointRect(pen: MapPen): { x: number; y: number; width: number; height: number } | null { + if (!pen.x || !pen.y || !pen.width || !pen.height) { + return null; + } + return { + x: pen.x + pen.width / 2, + y: pen.y + pen.height / 2, + width: pen.width, + height: pen.height, + }; + } + + /** + * 手动检查并更新指定区域的点位绑定 + * 当用户调整区域大小后,可以调用此方法手动触发检查 + * @param areaId 区域ID + * @param getPenById 根据ID获取图形对象的函数 + * @param findPoints 查找点位的函数 + * @param updateArea 更新区域的函数 + */ + public checkAndUpdateAreaPoints( + areaId: string, + getPenById: (id: string) => MapPen | undefined, + findPoints: (type: string) => MapPen[], + updateArea: (id: string, info: Partial) => void, + ): void { + const pen = getPenById(areaId); + if (!pen) { + return; + } + + this.handleAreaSizeChange(pen, findPoints, updateArea); + } + + /** + * 检查并更新所有互斥区和非互斥区的点位绑定 + * 用于批量检查和更新 + * @param findAreas 查找区域的函数 + * @param findPoints 查找点位的函数 + * @param updateArea 更新区域的函数 + */ + public checkAndUpdateAllAreas( + findAreas: (type: string) => MapPen[], + findPoints: (type: string) => MapPen[], + updateArea: (id: string, info: Partial) => void, + ): void { + const allAreas = findAreas('area') as MapPen[]; + + allAreas.forEach((area) => { + if (area.area?.type && [MapAreaType.互斥区, MapAreaType.非互斥区].includes(area.area.type)) { + this.handleAreaSizeChange(area, findPoints, updateArea); + } + }); + } +} diff --git a/src/services/editor.service.ts b/src/services/editor.service.ts index dcd850b..92c54fe 100644 --- a/src/services/editor.service.ts +++ b/src/services/editor.service.ts @@ -29,6 +29,7 @@ import { clone, get, isEmpty, isNil, isString, nth, omitBy, pick, remove, some } import { BehaviorSubject, debounceTime, filter, map, Subject, switchMap } from 'rxjs'; import { reactive, watch } from 'vue'; +import { AreaOperationService } from './area-operation.service'; import { LayerManagerService } from './layer-manager.service'; /** @@ -46,6 +47,9 @@ export class EditorService extends Meta2d { /** 图层管理服务实例 */ private readonly layerManager: LayerManagerService; + /** 区域操作服务实例 */ + private readonly areaOperationService!: AreaOperationService; + //#region 场景文件管理 /** * 加载场景文件到编辑器 @@ -782,6 +786,15 @@ export class EditorService extends Meta2d { /** 画布变化事件流,用于触发响应式数据更新 */ readonly #change$$ = new Subject(); + /** 区域大小变化防抖处理的事件流 */ + readonly #areaSizeChange$$ = new Subject(); + + /** 防抖处理后的区域大小变化事件,延迟500ms执行 */ + readonly #debouncedAreaSizeChange = this.#areaSizeChange$$.pipe( + debounceTime(500), // 500ms防抖延迟 + map((pen) => pen), + ); + /** 当前选中的图形对象,响应式更新 */ public readonly current = useObservable( this.#change$$.pipe( @@ -1267,6 +1280,9 @@ export class EditorService extends Meta2d { // 初始化图层管理服务 this.layerManager = new LayerManagerService(this); + // 初始化区域操作服务 + this.areaOperationService = new AreaOperationService(); + // 禁用第6个子元素的拖放功能 (container.children.item(5)).ondrop = null; // 监听所有画布事件 @@ -1280,6 +1296,15 @@ export class EditorService extends Meta2d { (v) => this.#load(v), { immediate: true }, ); + + // 订阅防抖处理后的区域大小变化事件 + this.#debouncedAreaSizeChange.subscribe((pen: MapPen) => { + this.areaOperationService.handleAreaSizeChange( + pen, + (type) => this.find(type), + (id, info) => this.updateArea(id, info), + ); + }); } #load(theme: string): void { @@ -1330,6 +1355,11 @@ export class EditorService extends Meta2d { break; case 'valueUpdate': this.#change$$.next(true); + // 检查是否是区域属性更新 + if (v && v.area?.type && ['area'].includes(v.tags?.[0] || '')) { + // 发送到防抖流,避免频繁触发 + this.#areaSizeChange$$.next(v); + } break; case 'active': @@ -1342,13 +1372,60 @@ export class EditorService extends Meta2d { case 'mouseup': this.#mouse$$.next({ type: e, value: pick(this.getPenRect(v), 'x', 'y') }); break; + // 监听区域调整大小事件 + case 'resizePens': { + // resizePens 事件的目标可能是 undefined,需要从 store.active 获取 + const activePens = this.store.active; + if (activePens && activePens.length > 0) { + activePens.forEach((pen: MapPen) => { + if (pen.area?.type && ['area'].includes(pen.tags?.[0] || '')) { + // 发送到防抖流,避免频繁触发 + this.#areaSizeChange$$.next(pen); + } + }); + } + break; + } + + // 监听区域线条更新事件,这通常是区域位置变化时触发的 + case 'updateLines': + if (v && v.area?.type && ['area'].includes(v.tags?.[0] || '')) { + // 发送到防抖流,避免频繁触发 + this.#areaSizeChange$$.next(v); + } + break; default: - // console.log(e, v); break; } } + /** + * 手动检查并更新指定区域的点位绑定 + * 当用户调整区域大小后,可以调用此方法手动触发检查 + * @param areaId 区域ID + */ + public checkAndUpdateAreaPoints(areaId: string): void { + this.areaOperationService.checkAndUpdateAreaPoints( + areaId, + (id) => this.getPenById(id), + (type) => this.find(type), + (id, info) => this.updateArea(id, info), + ); + } + + /** + * 检查并更新所有互斥区和非互斥区的点位绑定 + * 用于批量检查和更新 + */ + public checkAndUpdateAllAreas(): void { + this.areaOperationService.checkAndUpdateAllAreas( + (type) => this.find(type), + (type) => this.find(type), + (id, info) => this.updateArea(id, info), + ); + } + #register() { this.register({ line: () => new Path2D() }); this.registerCanvasDraw({ point: drawPoint, line: drawLine, area: drawArea, robot: drawRobot });