11/**
22 * Sparse grid for pathfinding - stores only obstacles, computes walkability on demand
33 * Supports incremental updates for efficient node dragging
4+ *
5+ * Performance: spatial hash uses numeric bucket keys to avoid string GC
6+ * in the hot isWalkableAt path (called thousands of times per A* run).
47 */
58
69import type { RoutingContext , Bounds , PortStub } from './types' ;
@@ -10,6 +13,14 @@ import { GRID_SIZE, ROUTING_MARGIN } from './constants';
1013/** Size of spatial hash buckets (in grid cells) */
1114const SPATIAL_BUCKET_SIZE = 10 ;
1215
16+ /**
17+ * Encode bucket coordinates into a single number.
18+ * Bucket coords are small (typically -50..+50), so 10_000 offset is plenty.
19+ */
20+ function encodeBucket ( bx : number , by : number ) : number {
21+ return ( bx + 10_000 ) * 20_001 + ( by + 10_000 ) ;
22+ }
23+
1324/**
1425 * Convert world coordinates to grid coordinates
1526 * Since everything is grid-aligned, this is a simple division
@@ -57,7 +68,7 @@ function boundsToObstacle(bounds: Bounds, offsetX: number, offsetY: number): Gri
5768
5869/**
5970 * Sparse grid that computes walkability on-demand from obstacle list
60- * No matrix storage - O(obstacles) memory instead of O(width × height)
71+ * No matrix storage - O(obstacles) memory instead of O(width x height)
6172 * Supports incremental updates - O(1) to update a single node
6273 * Effectively unbounded - only obstacles block movement
6374 */
@@ -71,33 +82,33 @@ export class SparseGrid {
7182 /** Port stub obstacles (rebuilt when stubs change) */
7283 private portStubObstacles : GridObstacle [ ] = [ ] ;
7384
74- /** Spatial hash: bucket key -> set of node IDs with obstacles in that bucket */
75- private spatialHash : Map < string , Set < string > > = new Map ( ) ;
85+ /** Spatial hash: numeric bucket key -> set of node IDs with obstacles in that bucket */
86+ private spatialHash : Map < number , Set < string > > = new Map ( ) ;
7687
7788 constructor ( context ?: RoutingContext ) {
7889 if ( context ) {
7990 this . initFromContext ( context ) ;
8091 }
8192 }
8293
83- /** Get bucket key for a grid coordinate */
84- private getBucketKey ( gx : number , gy : number ) : string {
94+ /** Get numeric bucket key for a grid coordinate */
95+ private getBucketKey ( gx : number , gy : number ) : number {
8596 const bx = Math . floor ( gx / SPATIAL_BUCKET_SIZE ) ;
8697 const by = Math . floor ( gy / SPATIAL_BUCKET_SIZE ) ;
87- return ` ${ bx } , ${ by } ` ;
98+ return encodeBucket ( bx , by ) ;
8899 }
89100
90101 /** Get all bucket keys that an obstacle overlaps */
91- private getObstacleBuckets ( obs : GridObstacle ) : string [ ] {
92- const keys : string [ ] = [ ] ;
102+ private getObstacleBuckets ( obs : GridObstacle ) : number [ ] {
103+ const keys : number [ ] = [ ] ;
93104 const minBx = Math . floor ( obs . minGx / SPATIAL_BUCKET_SIZE ) ;
94105 const maxBx = Math . floor ( obs . maxGx / SPATIAL_BUCKET_SIZE ) ;
95106 const minBy = Math . floor ( obs . minGy / SPATIAL_BUCKET_SIZE ) ;
96107 const maxBy = Math . floor ( obs . maxGy / SPATIAL_BUCKET_SIZE ) ;
97108
98109 for ( let bx = minBx ; bx <= maxBx ; bx ++ ) {
99110 for ( let by = minBy ; by <= maxBy ; by ++ ) {
100- keys . push ( ` ${ bx } , ${ by } ` ) ;
111+ keys . push ( encodeBucket ( bx , by ) ) ;
101112 }
102113 }
103114 return keys ;
0 commit comments