feat: 添加区域操作服务,支持区域大小变化的防抖处理与更新功能

This commit is contained in:
xudan 2025-08-27 11:28:40 +08:00
parent d48a96f36b
commit 3ce3e674ae
2 changed files with 264 additions and 1 deletions

View File

@ -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<MapAreaInfo>) => 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<MapAreaInfo>) => 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<MapAreaInfo>) => 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);
}
});
}
}

View File

@ -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<boolean>();
/** 区域大小变化防抖处理的事件流 */
readonly #areaSizeChange$$ = new Subject<MapPen>();
/** 防抖处理后的区域大小变化事件延迟500ms执行 */
readonly #debouncedAreaSizeChange = this.#areaSizeChange$$.pipe(
debounceTime(500), // 500ms防抖延迟
map((pen) => pen),
);
/** 当前选中的图形对象,响应式更新 */
public readonly current = useObservable<MapPen>(
this.#change$$.pipe(
@ -1267,6 +1280,9 @@ export class EditorService extends Meta2d {
// 初始化图层管理服务
this.layerManager = new LayerManagerService(this);
// 初始化区域操作服务
this.areaOperationService = new AreaOperationService();
// 禁用第6个子元素的拖放功能
(<HTMLDivElement>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 });