refactor: reorganize package structure and decouple framework packages (#338)

* refactor: reorganize package structure and decouple framework packages

## Package Structure Reorganization
- Reorganized 55 packages into categorized subdirectories:
  - packages/framework/ - Generic framework (Laya/Cocos compatible)
  - packages/engine/ - ESEngine core modules
  - packages/rendering/ - Rendering modules (WASM dependent)
  - packages/physics/ - Physics modules
  - packages/streaming/ - World streaming
  - packages/network-ext/ - Network extensions
  - packages/editor/ - Editor framework and plugins
  - packages/rust/ - Rust WASM engine
  - packages/tools/ - Build tools and SDK

## Framework Package Decoupling
- Decoupled behavior-tree and blueprint packages from ESEngine dependencies
- Created abstracted interfaces (IBTAssetManager, IBehaviorTreeAssetContent)
- ESEngine-specific code moved to esengine/ subpath exports
- Framework packages now usable with Cocos/Laya without ESEngine

## CI Configuration
- Updated CI to only type-check and lint framework packages
- Added type-check:framework and lint:framework scripts

## Breaking Changes
- Package import paths changed due to directory reorganization
- ESEngine integrations now use subpath imports (e.g., '@esengine/behavior-tree/esengine')

* fix: update es-engine file path after directory reorganization

* docs: update README to focus on framework over engine

* ci: only build framework packages, remove Rust/WASM dependencies

* fix: remove esengine subpath from behavior-tree and blueprint builds

ESEngine integration code will only be available in full engine builds.
Framework packages are now purely engine-agnostic.

* fix: move network-protocols to framework, build both in CI

* fix: update workflow paths from packages/core to packages/framework/core

* fix: exclude esengine folder from type-check in behavior-tree and blueprint

* fix: update network tsconfig references to new paths

* fix: add test:ci:framework to only test framework packages in CI

* fix: only build core and math npm packages in CI

* fix: exclude test files from CodeQL and fix string escaping security issue
This commit is contained in:
YHH
2025-12-26 14:50:35 +08:00
committed by GitHub
parent a84ff902e4
commit 155411e743
1936 changed files with 4147 additions and 11578 deletions

View File

@@ -0,0 +1,615 @@
/**
* @zh 导航网格实现
* @en NavMesh Implementation
*/
import type {
IPathfindingMap,
IPathNode,
IPoint,
IPathResult,
IPathfindingOptions
} from '../core/IPathfinding';
import { createPoint, euclideanDistance, EMPTY_PATH_RESULT, DEFAULT_PATHFINDING_OPTIONS } from '../core/IPathfinding';
import { BinaryHeap } from '../core/BinaryHeap';
// =============================================================================
// 导航多边形 | Navigation Polygon
// =============================================================================
/**
* @zh 导航多边形
* @en Navigation polygon
*/
export interface INavPolygon {
/** @zh 多边形ID @en Polygon ID */
readonly id: number;
/** @zh 顶点列表 @en Vertex list */
readonly vertices: readonly IPoint[];
/** @zh 中心点 @en Center point */
readonly center: IPoint;
/** @zh 邻居多边形ID @en Neighbor polygon IDs */
readonly neighbors: readonly number[];
/** @zh 到邻居的共享边 @en Shared edges to neighbors */
readonly portals: ReadonlyMap<number, IPortal>;
}
/**
* @zh 入口(两个多边形之间的共享边)
* @en Portal (shared edge between two polygons)
*/
export interface IPortal {
/** @zh 边的左端点 @en Left endpoint of edge */
readonly left: IPoint;
/** @zh 边的右端点 @en Right endpoint of edge */
readonly right: IPoint;
}
// =============================================================================
// 导航网格节点 | NavMesh Node
// =============================================================================
/**
* @zh 导航网格节点(包装多边形)
* @en NavMesh node (wraps polygon)
*/
class NavMeshNode implements IPathNode {
readonly id: number;
readonly position: IPoint;
readonly cost: number;
readonly walkable: boolean;
readonly polygon: INavPolygon;
constructor(polygon: INavPolygon) {
this.id = polygon.id;
this.position = polygon.center;
this.cost = 1;
this.walkable = true;
this.polygon = polygon;
}
}
// =============================================================================
// 导航网格 | Navigation Mesh
// =============================================================================
/**
* @zh 导航网格
* @en Navigation Mesh
*
* @zh 使用凸多边形网格进行高效寻路,适合复杂地形
* @en Uses convex polygon mesh for efficient pathfinding, suitable for complex terrain
*
* @example
* ```typescript
* const navmesh = new NavMesh();
*
* // Add polygons
* navmesh.addPolygon([
* { x: 0, y: 0 }, { x: 10, y: 0 },
* { x: 10, y: 10 }, { x: 0, y: 10 }
* ]);
*
* // Build connections
* navmesh.build();
*
* // Find path
* const result = navmesh.findPath(1, 1, 8, 8);
* ```
*/
export class NavMesh implements IPathfindingMap {
private polygons: Map<number, INavPolygon> = new Map();
private nodes: Map<number, NavMeshNode> = new Map();
private nextId = 0;
/**
* @zh 添加导航多边形
* @en Add navigation polygon
*
* @returns @zh 多边形ID @en Polygon ID
*/
addPolygon(vertices: IPoint[], neighbors: number[] = []): number {
const id = this.nextId++;
const center = this.calculateCenter(vertices);
const polygon: INavPolygon = {
id,
vertices,
center,
neighbors,
portals: new Map()
};
this.polygons.set(id, polygon);
this.nodes.set(id, new NavMeshNode(polygon));
return id;
}
/**
* @zh 设置两个多边形之间的连接
* @en Set connection between two polygons
*/
setConnection(
polyA: number,
polyB: number,
portal: IPortal
): void {
const polygonA = this.polygons.get(polyA);
const polygonB = this.polygons.get(polyB);
if (!polygonA || !polygonB) {
return;
}
// Update neighbors and portals
const neighborsA = [...polygonA.neighbors];
const portalsA = new Map(polygonA.portals);
if (!neighborsA.includes(polyB)) {
neighborsA.push(polyB);
}
portalsA.set(polyB, portal);
this.polygons.set(polyA, {
...polygonA,
neighbors: neighborsA,
portals: portalsA
});
// Reverse portal for the other direction
const reversePortal: IPortal = {
left: portal.right,
right: portal.left
};
const neighborsB = [...polygonB.neighbors];
const portalsB = new Map(polygonB.portals);
if (!neighborsB.includes(polyA)) {
neighborsB.push(polyA);
}
portalsB.set(polyA, reversePortal);
this.polygons.set(polyB, {
...polygonB,
neighbors: neighborsB,
portals: portalsB
});
}
/**
* @zh 自动检测并建立相邻多边形的连接
* @en Auto-detect and build connections between adjacent polygons
*/
build(): void {
const polygonList = Array.from(this.polygons.values());
for (let i = 0; i < polygonList.length; i++) {
for (let j = i + 1; j < polygonList.length; j++) {
const polyA = polygonList[i];
const polyB = polygonList[j];
const sharedEdge = this.findSharedEdge(polyA.vertices, polyB.vertices);
if (sharedEdge) {
this.setConnection(polyA.id, polyB.id, sharedEdge);
}
}
}
}
/**
* @zh 查找两个多边形的共享边
* @en Find shared edge between two polygons
*/
private findSharedEdge(
verticesA: readonly IPoint[],
verticesB: readonly IPoint[]
): IPortal | null {
const epsilon = 0.0001;
for (let i = 0; i < verticesA.length; i++) {
const a1 = verticesA[i];
const a2 = verticesA[(i + 1) % verticesA.length];
for (let j = 0; j < verticesB.length; j++) {
const b1 = verticesB[j];
const b2 = verticesB[(j + 1) % verticesB.length];
// Check if edges match (in either direction)
const match1 =
Math.abs(a1.x - b2.x) < epsilon &&
Math.abs(a1.y - b2.y) < epsilon &&
Math.abs(a2.x - b1.x) < epsilon &&
Math.abs(a2.y - b1.y) < epsilon;
const match2 =
Math.abs(a1.x - b1.x) < epsilon &&
Math.abs(a1.y - b1.y) < epsilon &&
Math.abs(a2.x - b2.x) < epsilon &&
Math.abs(a2.y - b2.y) < epsilon;
if (match1 || match2) {
return {
left: a1,
right: a2
};
}
}
}
return null;
}
/**
* @zh 计算多边形中心
* @en Calculate polygon center
*/
private calculateCenter(vertices: readonly IPoint[]): IPoint {
let x = 0;
let y = 0;
for (const v of vertices) {
x += v.x;
y += v.y;
}
return createPoint(x / vertices.length, y / vertices.length);
}
/**
* @zh 查找包含点的多边形
* @en Find polygon containing point
*/
findPolygonAt(x: number, y: number): INavPolygon | null {
for (const polygon of this.polygons.values()) {
if (this.isPointInPolygon(x, y, polygon.vertices)) {
return polygon;
}
}
return null;
}
/**
* @zh 检查点是否在多边形内
* @en Check if point is inside polygon
*/
private isPointInPolygon(x: number, y: number, vertices: readonly IPoint[]): boolean {
let inside = false;
const n = vertices.length;
for (let i = 0, j = n - 1; i < n; j = i++) {
const xi = vertices[i].x;
const yi = vertices[i].y;
const xj = vertices[j].x;
const yj = vertices[j].y;
if (
yi > y !== yj > y &&
x < ((xj - xi) * (y - yi)) / (yj - yi) + xi
) {
inside = !inside;
}
}
return inside;
}
// ==========================================================================
// IPathfindingMap 接口实现 | IPathfindingMap Interface Implementation
// ==========================================================================
getNodeAt(x: number, y: number): IPathNode | null {
const polygon = this.findPolygonAt(x, y);
return polygon ? this.nodes.get(polygon.id) ?? null : null;
}
getNeighbors(node: IPathNode): IPathNode[] {
const navNode = node as NavMeshNode;
const neighbors: IPathNode[] = [];
for (const neighborId of navNode.polygon.neighbors) {
const neighbor = this.nodes.get(neighborId);
if (neighbor) {
neighbors.push(neighbor);
}
}
return neighbors;
}
heuristic(a: IPoint, b: IPoint): number {
return euclideanDistance(a, b);
}
getMovementCost(from: IPathNode, to: IPathNode): number {
return euclideanDistance(from.position, to.position);
}
isWalkable(x: number, y: number): boolean {
return this.findPolygonAt(x, y) !== null;
}
// ==========================================================================
// 寻路 | Pathfinding
// ==========================================================================
/**
* @zh 在导航网格上寻路
* @en Find path on navigation mesh
*/
findPath(
startX: number,
startY: number,
endX: number,
endY: number,
options?: IPathfindingOptions
): IPathResult {
const opts = { ...DEFAULT_PATHFINDING_OPTIONS, ...options };
const startPolygon = this.findPolygonAt(startX, startY);
const endPolygon = this.findPolygonAt(endX, endY);
if (!startPolygon || !endPolygon) {
return EMPTY_PATH_RESULT;
}
// Same polygon
if (startPolygon.id === endPolygon.id) {
return {
found: true,
path: [createPoint(startX, startY), createPoint(endX, endY)],
cost: euclideanDistance(
createPoint(startX, startY),
createPoint(endX, endY)
),
nodesSearched: 1
};
}
// A* on polygon graph
const polygonPath = this.findPolygonPath(startPolygon, endPolygon, opts);
if (!polygonPath.found) {
return EMPTY_PATH_RESULT;
}
// Convert polygon path to point path using funnel algorithm
const start = createPoint(startX, startY);
const end = createPoint(endX, endY);
const pointPath = this.funnelPath(start, end, polygonPath.polygons);
return {
found: true,
path: pointPath,
cost: this.calculatePathLength(pointPath),
nodesSearched: polygonPath.nodesSearched
};
}
/**
* @zh 在多边形图上寻路
* @en Find path on polygon graph
*/
private findPolygonPath(
start: INavPolygon,
end: INavPolygon,
opts: Required<IPathfindingOptions>
): { found: boolean; polygons: INavPolygon[]; nodesSearched: number } {
interface AStarState {
polygon: INavPolygon;
g: number;
f: number;
parent: AStarState | null;
}
const openList = new BinaryHeap<AStarState>((a, b) => a.f - b.f);
const closed = new Set<number>();
const states = new Map<number, AStarState>();
const startState: AStarState = {
polygon: start,
g: 0,
f: euclideanDistance(start.center, end.center) * opts.heuristicWeight,
parent: null
};
states.set(start.id, startState);
openList.push(startState);
let nodesSearched = 0;
while (!openList.isEmpty && nodesSearched < opts.maxNodes) {
const current = openList.pop()!;
nodesSearched++;
if (current.polygon.id === end.id) {
// Reconstruct path
const path: INavPolygon[] = [];
let state: AStarState | null = current;
while (state) {
path.unshift(state.polygon);
state = state.parent;
}
return { found: true, polygons: path, nodesSearched };
}
closed.add(current.polygon.id);
for (const neighborId of current.polygon.neighbors) {
if (closed.has(neighborId)) {
continue;
}
const neighborPolygon = this.polygons.get(neighborId);
if (!neighborPolygon) {
continue;
}
const g = current.g + euclideanDistance(
current.polygon.center,
neighborPolygon.center
);
let neighborState = states.get(neighborId);
if (!neighborState) {
neighborState = {
polygon: neighborPolygon,
g,
f: g + euclideanDistance(neighborPolygon.center, end.center) * opts.heuristicWeight,
parent: current
};
states.set(neighborId, neighborState);
openList.push(neighborState);
} else if (g < neighborState.g) {
neighborState.g = g;
neighborState.f = g + euclideanDistance(neighborPolygon.center, end.center) * opts.heuristicWeight;
neighborState.parent = current;
openList.update(neighborState);
}
}
}
return { found: false, polygons: [], nodesSearched };
}
/**
* @zh 使用漏斗算法优化路径
* @en Optimize path using funnel algorithm
*/
private funnelPath(
start: IPoint,
end: IPoint,
polygons: INavPolygon[]
): IPoint[] {
if (polygons.length <= 1) {
return [start, end];
}
// Collect portals
const portals: IPortal[] = [];
for (let i = 0; i < polygons.length - 1; i++) {
const portal = polygons[i].portals.get(polygons[i + 1].id);
if (portal) {
portals.push(portal);
}
}
if (portals.length === 0) {
return [start, end];
}
// Simple string pulling algorithm
const path: IPoint[] = [start];
let apex = start;
let leftIndex = 0;
let rightIndex = 0;
let left = portals[0].left;
let right = portals[0].right;
for (let i = 1; i <= portals.length; i++) {
const nextLeft = i < portals.length ? portals[i].left : end;
const nextRight = i < portals.length ? portals[i].right : end;
// Update right
if (this.triArea2(apex, right, nextRight) <= 0) {
if (apex === right || this.triArea2(apex, left, nextRight) > 0) {
right = nextRight;
rightIndex = i;
} else {
path.push(left);
apex = left;
leftIndex = rightIndex = leftIndex;
left = right = apex;
i = leftIndex;
continue;
}
}
// Update left
if (this.triArea2(apex, left, nextLeft) >= 0) {
if (apex === left || this.triArea2(apex, right, nextLeft) < 0) {
left = nextLeft;
leftIndex = i;
} else {
path.push(right);
apex = right;
leftIndex = rightIndex = rightIndex;
left = right = apex;
i = rightIndex;
continue;
}
}
}
path.push(end);
return path;
}
/**
* @zh 计算三角形面积的两倍(用于判断点的相对位置)
* @en Calculate twice the triangle area (for point relative position)
*/
private triArea2(a: IPoint, b: IPoint, c: IPoint): number {
return (c.x - a.x) * (b.y - a.y) - (b.x - a.x) * (c.y - a.y);
}
/**
* @zh 计算路径总长度
* @en Calculate total path length
*/
private calculatePathLength(path: readonly IPoint[]): number {
let length = 0;
for (let i = 1; i < path.length; i++) {
length += euclideanDistance(path[i - 1], path[i]);
}
return length;
}
/**
* @zh 清空导航网格
* @en Clear navigation mesh
*/
clear(): void {
this.polygons.clear();
this.nodes.clear();
this.nextId = 0;
}
/**
* @zh 获取所有多边形
* @en Get all polygons
*/
getPolygons(): INavPolygon[] {
return Array.from(this.polygons.values());
}
/**
* @zh 获取多边形数量
* @en Get polygon count
*/
get polygonCount(): number {
return this.polygons.size;
}
}
// =============================================================================
// 工厂函数 | Factory Function
// =============================================================================
/**
* @zh 创建导航网格
* @en Create navigation mesh
*/
export function createNavMesh(): NavMesh {
return new NavMesh();
}

View File

@@ -0,0 +1,11 @@
/**
* @zh 导航网格模块
* @en NavMesh Module
*/
export {
NavMesh,
createNavMesh,
type INavPolygon,
type IPortal
} from './NavMesh';