template/packages-user/data-system/src/path/graph.ts

239 lines
7.5 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import {
DirectionMapper,
IDirectionDescriptor,
IDirectionMapper,
InternalDirectionGroup,
logger
} from '@motajs/common';
import { FaceDirection, IDataCommon } from '@user/data-common';
import {
ILayerLocation,
IMapLayer,
IMapState,
IPassCheckHandler,
IPassPredicate
} from '@user/data-base';
import { isNil } from 'lodash-es';
//#region 图结构
export interface IPathGraphEdge {
/** 本条边对应的移动方向 */
readonly dir: FaceDirection;
/** 边指向的节点索引,值为 y * width + x */
readonly to: number;
}
export interface IPathGraphNode {
/** 节点索引,值为 y * width + x */
readonly index: number;
/** 节点横坐标 */
readonly x: number;
/** 节点纵坐标 */
readonly y: number;
/** 节点对应的位置信息,用于损失计算 */
readonly block: ILayerLocation;
/** 该节点是否仅可作为路径终点,不可作为中间节点 */
readonly terminal: boolean;
/** 该节点的全部出边 */
readonly edges: readonly IPathGraphEdge[];
}
export interface IPathGraph {
/** 图宽度 */
readonly width: number;
/** 图高度 */
readonly height: number;
/** 图内全部节点,键为节点索引,值为 y * width + x */
readonly nodes: ReadonlyMap<number, IPathGraphNode>;
}
//#endregion
//#region 方向解析
/**
* 将方向描述器的坐标增量解析为对应的朝向
* @param x 横坐标增量
* @param y 纵坐标增量
*/
function directionOf(x: number, y: number): FaceDirection {
if (x === 0 && y === -1) return FaceDirection.Up;
if (x === 0 && y === 1) return FaceDirection.Down;
if (x === -1 && y === 0) return FaceDirection.Left;
if (x === 1 && y === 0) return FaceDirection.Right;
if (x === -1 && y === -1) return FaceDirection.LeftUp;
if (x === 1 && y === -1) return FaceDirection.RightUp;
if (x === -1 && y === 1) return FaceDirection.LeftDown;
if (x === 1 && y === 1) return FaceDirection.RightDown;
return FaceDirection.Unknown;
}
//#endregion
//#region 有向图构建
/**
* 寻路有向图构建器接口,声明图构建的绑定槽位与构建入口
*/
export interface IPathfindingGraphBuilder {
/**
* 绑定地图状态对象,用于解析图层所属楼层 id
* @param maps 地图状态对象,传入 `null` 解绑
*/
useMapState(maps: IMapState | null): void;
/**
* 绑定构建有向图所用的地图图层
* @param layer 地图图层对象,传入 `null` 解绑
*/
useMapLayer(layer: IMapLayer | null): void;
/**
* 注入判定边可行性的通行性谓词
* @param predicate 通行性谓词,传入 `null` 解绑
*/
usePassPredicate(predicate: IPassPredicate | null): void;
/**
* 设置邻域使用的方向组
* @param group 朝向组
*/
useDirGroup(group: number): void;
/**
* 构建有向图。图层未绑定时告警并返回空图,
* 不包含任何节点与边
* @returns 构建的有向图
*/
build(): IPathGraph;
}
/**
* 寻路有向图构建器,将地图图层转换为以通行性谓词判定边的有向图。
* 邻域方向由注入的方向组决定,默认仅包含四正交方向;
* 谓词未注入时所有边均不可通行
*/
export class PathfindingGraphBuilder implements IPathfindingGraphBuilder {
/** 绑定的地图状态对象,用于解析图层所属楼层 id */
private maps: IMapState | null = null;
/** 绑定的地图图层,图节点来源 */
private layer: IMapLayer | null = null;
/** 注入的通行性谓词,用于判定边的可行性与终端节点 */
private predicate: IPassPredicate | null = null;
/** 邻域方向组别,默认四正交方向 */
private group: number = InternalDirectionGroup.Dir4;
/** 方向组解析对象 */
private readonly mapper: IDirectionMapper = new DirectionMapper();
useMapState(maps: IMapState | null): void {
this.maps = maps;
}
useMapLayer(layer: IMapLayer | null): void {
this.layer = layer;
}
usePassPredicate(predicate: IPassPredicate | null): void {
this.predicate = predicate;
}
useDirGroup(group: number): void {
this.group = group;
}
build(): IPathGraph {
const layer = this.layer;
if (isNil(layer)) {
logger.warn(173);
return { width: 0, height: 0, nodes: new Map() };
}
const width = layer.width;
const height = layer.height;
const floorId = this.resolveFloorId();
const state: IDataCommon = layer.state;
const blocks: (ILayerLocation | null)[] = new Array(
width * height
).fill(null);
// 收集图内全部图块作为图节点
for (let y = 0; y < height; y++) {
for (let x = 0; x < width; x++) {
if (!layer.inMap(x, y)) continue;
const loc = layer.getLocationData(x, y);
if (!loc) continue;
blocks[y * width + x] = loc;
}
}
const terminals: Set<number> = new Set();
const adjacency: Map<number, IPathGraphEdge[]> = new Map();
const dirs: IDirectionDescriptor[] = [...this.mapper.map(this.group)];
// 逐节点判定邻域边可行性
for (let index = 0; index < blocks.length; index++) {
const block = blocks[index];
if (!block) continue;
const x = index % width;
const y = Math.floor(index / width);
const edges: IPathGraphEdge[] = [];
for (const desc of dirs) {
const dir = directionOf(desc.x, desc.y);
if (dir === FaceDirection.Unknown) continue;
const nx = x + desc.x;
const ny = y + desc.y;
if (!layer.inMap(nx, ny)) continue;
const next = blocks[ny * width + nx];
if (!next) continue;
const handler: IPassCheckHandler = {
currLoc: block.locator,
nextLoc: next.locator,
direction: dir,
floorId,
state
};
const predicate = this.predicate;
if (isNil(predicate) || !predicate.canPass(handler)) continue;
if (predicate.shouldHit(handler)) {
terminals.add(ny * width + nx);
}
edges.push({ dir, to: ny * width + nx });
}
adjacency.set(index, edges);
}
const nodes: Map<number, IPathGraphNode> = new Map();
for (let index = 0; index < blocks.length; index++) {
const block = blocks[index];
if (!block) continue;
nodes.set(index, {
index,
x: index % width,
y: Math.floor(index / width),
block,
terminal: terminals.has(index),
edges: adjacency.get(index) ?? []
});
}
return { width, height, nodes };
}
/**
* 解析绑定图层所属的楼层 id
* @returns 楼层 id无法解析时为 `undefined`
*/
private resolveFloorId(): string | undefined {
const maps = this.maps;
const layer = this.layer;
if (!maps || !layer) return undefined;
for (const [floorId, map] of maps.iterateAllMaps()) {
if (map === layer.map) return floorId;
}
return undefined;
}
}
//#endregion