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:
396
packages/framework/spatial/src/GridSpatialIndex.ts
Normal file
396
packages/framework/spatial/src/GridSpatialIndex.ts
Normal file
@@ -0,0 +1,396 @@
|
||||
/**
|
||||
* @zh 网格空间索引
|
||||
* @en Grid Spatial Index
|
||||
*
|
||||
* @zh 基于均匀网格的空间索引实现
|
||||
* @en Uniform grid based spatial index implementation
|
||||
*/
|
||||
|
||||
import type { IVector2 } from '@esengine/ecs-framework-math';
|
||||
import type {
|
||||
ISpatialIndex,
|
||||
IBounds,
|
||||
IRaycastHit,
|
||||
SpatialFilter
|
||||
} from './ISpatialQuery';
|
||||
import {
|
||||
createBoundsFromCircle,
|
||||
boundsIntersectsCircle,
|
||||
distanceSquared,
|
||||
distance
|
||||
} from './ISpatialQuery';
|
||||
|
||||
// =============================================================================
|
||||
// 网格项 | Grid Item
|
||||
// =============================================================================
|
||||
|
||||
/**
|
||||
* @zh 网格中的项
|
||||
* @en Item in grid
|
||||
*/
|
||||
interface GridItem<T> {
|
||||
item: T;
|
||||
position: IVector2;
|
||||
cellKey: string;
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// 网格空间索引 | Grid Spatial Index
|
||||
// =============================================================================
|
||||
|
||||
/**
|
||||
* @zh 网格空间索引配置
|
||||
* @en Grid spatial index configuration
|
||||
*/
|
||||
export interface GridSpatialIndexConfig {
|
||||
/**
|
||||
* @zh 网格单元格大小
|
||||
* @en Grid cell size
|
||||
*/
|
||||
cellSize: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* @zh 网格空间索引实现
|
||||
* @en Grid spatial index implementation
|
||||
*
|
||||
* @zh 使用均匀网格进行空间划分,适合对象分布均匀的场景
|
||||
* @en Uses uniform grid for spatial partitioning, suitable for evenly distributed objects
|
||||
*/
|
||||
export class GridSpatialIndex<T> implements ISpatialIndex<T> {
|
||||
private readonly _cellSize: number;
|
||||
private readonly _cells: Map<string, Set<GridItem<T>>> = new Map();
|
||||
private readonly _itemMap: Map<T, GridItem<T>> = new Map();
|
||||
|
||||
constructor(config: GridSpatialIndexConfig) {
|
||||
this._cellSize = config.cellSize;
|
||||
}
|
||||
|
||||
// =========================================================================
|
||||
// ISpatialIndex 实现 | ISpatialIndex Implementation
|
||||
// =========================================================================
|
||||
|
||||
get count(): number {
|
||||
return this._itemMap.size;
|
||||
}
|
||||
|
||||
/**
|
||||
* @zh 插入对象
|
||||
* @en Insert object
|
||||
*/
|
||||
insert(item: T, position: IVector2): void {
|
||||
if (this._itemMap.has(item)) {
|
||||
this.update(item, position);
|
||||
return;
|
||||
}
|
||||
|
||||
const cellKey = this._getCellKey(position);
|
||||
const gridItem: GridItem<T> = { item, position: { x: position.x, y: position.y }, cellKey };
|
||||
|
||||
this._itemMap.set(item, gridItem);
|
||||
this._addToCell(cellKey, gridItem);
|
||||
}
|
||||
|
||||
/**
|
||||
* @zh 移除对象
|
||||
* @en Remove object
|
||||
*/
|
||||
remove(item: T): boolean {
|
||||
const gridItem = this._itemMap.get(item);
|
||||
if (!gridItem) {
|
||||
return false;
|
||||
}
|
||||
|
||||
this._removeFromCell(gridItem.cellKey, gridItem);
|
||||
this._itemMap.delete(item);
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* @zh 更新对象位置
|
||||
* @en Update object position
|
||||
*/
|
||||
update(item: T, newPosition: IVector2): boolean {
|
||||
const gridItem = this._itemMap.get(item);
|
||||
if (!gridItem) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const newCellKey = this._getCellKey(newPosition);
|
||||
|
||||
if (newCellKey !== gridItem.cellKey) {
|
||||
this._removeFromCell(gridItem.cellKey, gridItem);
|
||||
gridItem.cellKey = newCellKey;
|
||||
this._addToCell(newCellKey, gridItem);
|
||||
}
|
||||
|
||||
gridItem.position = { x: newPosition.x, y: newPosition.y };
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* @zh 清空索引
|
||||
* @en Clear index
|
||||
*/
|
||||
clear(): void {
|
||||
this._cells.clear();
|
||||
this._itemMap.clear();
|
||||
}
|
||||
|
||||
/**
|
||||
* @zh 获取所有对象
|
||||
* @en Get all objects
|
||||
*/
|
||||
getAll(): T[] {
|
||||
return Array.from(this._itemMap.keys());
|
||||
}
|
||||
|
||||
// =========================================================================
|
||||
// ISpatialQuery 实现 | ISpatialQuery Implementation
|
||||
// =========================================================================
|
||||
|
||||
/**
|
||||
* @zh 查找半径内的所有对象
|
||||
* @en Find all objects within radius
|
||||
*/
|
||||
findInRadius(center: IVector2, radius: number, filter?: SpatialFilter<T>): T[] {
|
||||
const results: T[] = [];
|
||||
const radiusSq = radius * radius;
|
||||
const bounds = createBoundsFromCircle(center, radius);
|
||||
|
||||
this._forEachInBounds(bounds, (gridItem) => {
|
||||
const distSq = distanceSquared(center, gridItem.position);
|
||||
if (distSq <= radiusSq) {
|
||||
if (!filter || filter(gridItem.item)) {
|
||||
results.push(gridItem.item);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
return results;
|
||||
}
|
||||
|
||||
/**
|
||||
* @zh 查找矩形区域内的所有对象
|
||||
* @en Find all objects within rectangle
|
||||
*/
|
||||
findInRect(bounds: IBounds, filter?: SpatialFilter<T>): T[] {
|
||||
const results: T[] = [];
|
||||
|
||||
this._forEachInBounds(bounds, (gridItem) => {
|
||||
const pos = gridItem.position;
|
||||
if (pos.x >= bounds.minX && pos.x <= bounds.maxX &&
|
||||
pos.y >= bounds.minY && pos.y <= bounds.maxY) {
|
||||
if (!filter || filter(gridItem.item)) {
|
||||
results.push(gridItem.item);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
return results;
|
||||
}
|
||||
|
||||
/**
|
||||
* @zh 查找最近的对象
|
||||
* @en Find nearest object
|
||||
*/
|
||||
findNearest(center: IVector2, maxDistance?: number, filter?: SpatialFilter<T>): T | null {
|
||||
let nearest: T | null = null;
|
||||
let nearestDistSq = maxDistance !== undefined ? maxDistance * maxDistance : Infinity;
|
||||
|
||||
const searchRadius = maxDistance ?? this._cellSize * 10;
|
||||
const bounds = createBoundsFromCircle(center, searchRadius);
|
||||
|
||||
this._forEachInBounds(bounds, (gridItem) => {
|
||||
const distSq = distanceSquared(center, gridItem.position);
|
||||
if (distSq < nearestDistSq) {
|
||||
if (!filter || filter(gridItem.item)) {
|
||||
nearest = gridItem.item;
|
||||
nearestDistSq = distSq;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
return nearest;
|
||||
}
|
||||
|
||||
/**
|
||||
* @zh 查找最近的 K 个对象
|
||||
* @en Find K nearest objects
|
||||
*/
|
||||
findKNearest(center: IVector2, k: number, maxDistance?: number, filter?: SpatialFilter<T>): T[] {
|
||||
if (k <= 0) return [];
|
||||
|
||||
const candidates: Array<{ item: T; distSq: number }> = [];
|
||||
const maxDistSq = maxDistance !== undefined ? maxDistance * maxDistance : Infinity;
|
||||
const searchRadius = maxDistance ?? this._cellSize * 10;
|
||||
const bounds = createBoundsFromCircle(center, searchRadius);
|
||||
|
||||
this._forEachInBounds(bounds, (gridItem) => {
|
||||
const distSq = distanceSquared(center, gridItem.position);
|
||||
if (distSq <= maxDistSq) {
|
||||
if (!filter || filter(gridItem.item)) {
|
||||
candidates.push({ item: gridItem.item, distSq });
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
candidates.sort((a, b) => a.distSq - b.distSq);
|
||||
return candidates.slice(0, k).map(c => c.item);
|
||||
}
|
||||
|
||||
/**
|
||||
* @zh 射线检测
|
||||
* @en Raycast
|
||||
*/
|
||||
raycast(origin: IVector2, direction: IVector2, maxDistance: number, filter?: SpatialFilter<T>): IRaycastHit<T>[] {
|
||||
const hits: IRaycastHit<T>[] = [];
|
||||
const rayBounds = this._getRayBounds(origin, direction, maxDistance);
|
||||
|
||||
this._forEachInBounds(rayBounds, (gridItem) => {
|
||||
const hit = this._rayIntersectsPoint(origin, direction, gridItem.position, maxDistance);
|
||||
if (hit && hit.distance <= maxDistance) {
|
||||
if (!filter || filter(gridItem.item)) {
|
||||
hits.push({
|
||||
target: gridItem.item,
|
||||
point: hit.point,
|
||||
normal: hit.normal,
|
||||
distance: hit.distance
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
hits.sort((a, b) => a.distance - b.distance);
|
||||
return hits;
|
||||
}
|
||||
|
||||
/**
|
||||
* @zh 射线检测(仅返回第一个命中)
|
||||
* @en Raycast (return first hit only)
|
||||
*/
|
||||
raycastFirst(origin: IVector2, direction: IVector2, maxDistance: number, filter?: SpatialFilter<T>): IRaycastHit<T> | null {
|
||||
const hits = this.raycast(origin, direction, maxDistance, filter);
|
||||
return hits.length > 0 ? hits[0] : null;
|
||||
}
|
||||
|
||||
// =========================================================================
|
||||
// 私有方法 | Private Methods
|
||||
// =========================================================================
|
||||
|
||||
private _getCellKey(position: IVector2): string {
|
||||
const cellX = Math.floor(position.x / this._cellSize);
|
||||
const cellY = Math.floor(position.y / this._cellSize);
|
||||
return `${cellX},${cellY}`;
|
||||
}
|
||||
|
||||
private _getCellCoords(position: IVector2): { x: number; y: number } {
|
||||
return {
|
||||
x: Math.floor(position.x / this._cellSize),
|
||||
y: Math.floor(position.y / this._cellSize)
|
||||
};
|
||||
}
|
||||
|
||||
private _addToCell(cellKey: string, gridItem: GridItem<T>): void {
|
||||
let cell = this._cells.get(cellKey);
|
||||
if (!cell) {
|
||||
cell = new Set();
|
||||
this._cells.set(cellKey, cell);
|
||||
}
|
||||
cell.add(gridItem);
|
||||
}
|
||||
|
||||
private _removeFromCell(cellKey: string, gridItem: GridItem<T>): void {
|
||||
const cell = this._cells.get(cellKey);
|
||||
if (cell) {
|
||||
cell.delete(gridItem);
|
||||
if (cell.size === 0) {
|
||||
this._cells.delete(cellKey);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private _forEachInBounds(bounds: IBounds, callback: (item: GridItem<T>) => void): void {
|
||||
const minCell = this._getCellCoords({ x: bounds.minX, y: bounds.minY });
|
||||
const maxCell = this._getCellCoords({ x: bounds.maxX, y: bounds.maxY });
|
||||
|
||||
for (let x = minCell.x; x <= maxCell.x; x++) {
|
||||
for (let y = minCell.y; y <= maxCell.y; y++) {
|
||||
const cell = this._cells.get(`${x},${y}`);
|
||||
if (cell) {
|
||||
for (const gridItem of cell) {
|
||||
callback(gridItem);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private _getRayBounds(origin: IVector2, direction: IVector2, maxDistance: number): IBounds {
|
||||
const endX = origin.x + direction.x * maxDistance;
|
||||
const endY = origin.y + direction.y * maxDistance;
|
||||
|
||||
return {
|
||||
minX: Math.min(origin.x, endX),
|
||||
minY: Math.min(origin.y, endY),
|
||||
maxX: Math.max(origin.x, endX),
|
||||
maxY: Math.max(origin.y, endY)
|
||||
};
|
||||
}
|
||||
|
||||
private _rayIntersectsPoint(
|
||||
origin: IVector2,
|
||||
direction: IVector2,
|
||||
point: IVector2,
|
||||
_maxDistance: number,
|
||||
hitRadius: number = 0.5
|
||||
): { point: IVector2; normal: IVector2; distance: number } | null {
|
||||
const toPoint = { x: point.x - origin.x, y: point.y - origin.y };
|
||||
const projection = toPoint.x * direction.x + toPoint.y * direction.y;
|
||||
|
||||
if (projection < 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const closestX = origin.x + direction.x * projection;
|
||||
const closestY = origin.y + direction.y * projection;
|
||||
const distToLine = Math.sqrt(
|
||||
(point.x - closestX) * (point.x - closestX) +
|
||||
(point.y - closestY) * (point.y - closestY)
|
||||
);
|
||||
|
||||
if (distToLine <= hitRadius) {
|
||||
const hitDist = projection - Math.sqrt(hitRadius * hitRadius - distToLine * distToLine);
|
||||
if (hitDist >= 0) {
|
||||
const hitPoint = {
|
||||
x: origin.x + direction.x * hitDist,
|
||||
y: origin.y + direction.y * hitDist
|
||||
};
|
||||
const normal = {
|
||||
x: hitPoint.x - point.x,
|
||||
y: hitPoint.y - point.y
|
||||
};
|
||||
const normalLen = Math.sqrt(normal.x * normal.x + normal.y * normal.y);
|
||||
if (normalLen > 0) {
|
||||
normal.x /= normalLen;
|
||||
normal.y /= normalLen;
|
||||
}
|
||||
return { point: hitPoint, normal, distance: hitDist };
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// 工厂函数 | Factory Functions
|
||||
// =============================================================================
|
||||
|
||||
/**
|
||||
* @zh 创建网格空间索引
|
||||
* @en Create grid spatial index
|
||||
*/
|
||||
export function createGridSpatialIndex<T>(cellSize: number = 100): GridSpatialIndex<T> {
|
||||
return new GridSpatialIndex<T>({ cellSize });
|
||||
}
|
||||
Reference in New Issue
Block a user