@flare-engine/pathfinding 0.4.10 → 0.4.11

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.mjs CHANGED
@@ -37,25 +37,8 @@ function setCost(grid, x, y, cost) {
37
37
  if (x < 0 || y < 0 || x >= grid.width || y >= grid.height) return;
38
38
  grid.data[(y | 0) * grid.width + (x | 0)] = cost > 255 ? 255 : cost < 0 ? 0 : cost | 0;
39
39
  }
40
- // src/astar.ts
41
- const SQRT2$1 = Math.SQRT2;
42
- function manhattanDistance(ax, ay, bx, by) {
43
- return Math.abs(ax - bx) + Math.abs(ay - by);
44
- }
45
- function octileDistance(ax, ay, bx, by) {
46
- const dx = Math.abs(ax - bx);
47
- const dy = Math.abs(ay - by);
48
- return dx > dy ? dx + (SQRT2$1 - 1) * dy : dy + (SQRT2$1 - 1) * dx;
49
- }
50
- function chebyshevDistance(ax, ay, bx, by) {
51
- return Math.max(Math.abs(ax - bx), Math.abs(ay - by));
52
- }
53
- function euclideanDistance(ax, ay, bx, by) {
54
- const dx = ax - bx;
55
- const dy = ay - by;
56
- return Math.sqrt(dx * dx + dy * dy);
57
- }
58
- function heapCreate$1(capacity) {
40
+ // src/heap.ts
41
+ function heapCreate(capacity) {
59
42
  return {
60
43
  f: new Float64Array(capacity),
61
44
  idx: new Int32Array(capacity),
@@ -72,7 +55,7 @@ function heapCreate$1(capacity) {
72
55
  * non-optimal path. Growth is amortized O(1): allocation happens only when
73
56
  * capacity is reached, never per push.
74
57
  */
75
- function heapGrow$1(h) {
58
+ function heapGrow(h) {
76
59
  const next = h.f.length === 0 ? 16 : h.f.length * 2;
77
60
  const f = new Float64Array(next);
78
61
  f.set(h.f);
@@ -81,8 +64,8 @@ function heapGrow$1(h) {
81
64
  h.f = f;
82
65
  h.idx = idx;
83
66
  }
84
- function heapPush$1(h, f, nodeIdx) {
85
- if (h.size === h.f.length) heapGrow$1(h);
67
+ function heapPush(h, f, nodeIdx) {
68
+ if (h.size === h.f.length) heapGrow(h);
86
69
  let i = h.size++;
87
70
  h.f[i] = f;
88
71
  h.idx[i] = nodeIdx;
@@ -98,7 +81,7 @@ function heapPush$1(h, f, nodeIdx) {
98
81
  i = parent;
99
82
  }
100
83
  }
101
- function heapPop$1(h) {
84
+ function heapPop(h) {
102
85
  const result = h.idx[0];
103
86
  h.size--;
104
87
  if (h.size > 0) {
@@ -123,6 +106,65 @@ function heapPop$1(h) {
123
106
  }
124
107
  return result;
125
108
  }
109
+ // src/scratch.ts
110
+ const MAX_OPEN = 4294967294;
111
+ const scratches = /* @__PURE__ */ new WeakMap();
112
+ function createScratch(total) {
113
+ return {
114
+ total,
115
+ g: new Float64Array(total),
116
+ cameFrom: new Int32Array(total),
117
+ stamp: new Uint32Array(total),
118
+ open: 0,
119
+ heap: heapCreate(Math.min(total, 4096)),
120
+ succ: /* @__PURE__ */ new Int32Array(8),
121
+ inUse: false
122
+ };
123
+ }
124
+ /**
125
+ * Hand out the scratch for `grid`, ready for a new search: generation advanced
126
+ * (every cell reads as untouched), heap emptied, start cell untouched too — the
127
+ * caller stamps it.
128
+ *
129
+ * A grid whose stored scratch is already in use (a heuristic re-entered a
130
+ * search on the same grid) gets a fresh temporary scratch that is not stored.
131
+ */
132
+ function acquireScratch(grid) {
133
+ const total = grid.width * grid.height;
134
+ let s = scratches.get(grid);
135
+ if (s === void 0 || s.total !== total) {
136
+ s = createScratch(total);
137
+ scratches.set(grid, s);
138
+ } else if (s.inUse) s = createScratch(total);
139
+ s.inUse = true;
140
+ s.heap.size = 0;
141
+ if (s.open >= MAX_OPEN) {
142
+ s.stamp.fill(0);
143
+ s.open = 2;
144
+ } else s.open += 2;
145
+ return s;
146
+ }
147
+ function releaseScratch(s) {
148
+ s.inUse = false;
149
+ }
150
+ // src/astar.ts
151
+ const SQRT2$1 = Math.SQRT2;
152
+ function manhattanDistance(ax, ay, bx, by) {
153
+ return Math.abs(ax - bx) + Math.abs(ay - by);
154
+ }
155
+ function octileDistance(ax, ay, bx, by) {
156
+ const dx = Math.abs(ax - bx);
157
+ const dy = Math.abs(ay - by);
158
+ return dx > dy ? dx + (SQRT2$1 - 1) * dy : dy + (SQRT2$1 - 1) * dx;
159
+ }
160
+ function chebyshevDistance(ax, ay, bx, by) {
161
+ return Math.max(Math.abs(ax - bx), Math.abs(ay - by));
162
+ }
163
+ function euclideanDistance(ax, ay, bx, by) {
164
+ const dx = ax - bx;
165
+ const dy = ay - by;
166
+ return Math.sqrt(dx * dx + dy * dy);
167
+ }
126
168
  const DIR4_X = [
127
169
  0,
128
170
  1,
@@ -183,53 +225,58 @@ function findPath(grid, startX, startY, endX, endY, options) {
183
225
  const maxIter = options?.maxIterations ?? grid.width * grid.height;
184
226
  const heuristic = options?.heuristic ?? (diagonal ? octileDistance : manhattanDistance);
185
227
  const w = grid.width;
186
- const total = w * grid.height;
187
- const g = new Float64Array(total);
188
- g.fill(Number.POSITIVE_INFINITY);
189
- const closed = new Uint8Array(total);
190
- const cameFrom = new Int32Array(total);
191
- cameFrom.fill(-1);
192
228
  const startIdx = sy * w + sx;
193
229
  const endIdx = ey * w + ex;
230
+ const scratch = acquireScratch(grid);
231
+ const { g, cameFrom, stamp, heap } = scratch;
232
+ const openMark = scratch.open;
233
+ const closedMark = openMark + 1;
234
+ stamp[startIdx] = openMark;
194
235
  g[startIdx] = 0;
195
- const heap = heapCreate$1(Math.min(total, 4096));
196
- heapPush$1(heap, heuristic(sx, sy, ex, ey), startIdx);
236
+ cameFrom[startIdx] = -1;
197
237
  const dirX = diagonal ? DIR8_X : DIR4_X;
198
238
  const dirY = diagonal ? DIR8_Y : DIR4_Y;
199
239
  const dirCost = diagonal ? DIR8_COST : null;
200
240
  const dirCount = dirX.length;
201
241
  let explored = 0;
202
- while (heap.size > 0 && explored < maxIter) {
203
- const currentIdx = heapPop$1(heap);
204
- if (currentIdx === endIdx) return reconstructPath(cameFrom, w, currentIdx, g[currentIdx], explored);
205
- if (closed[currentIdx]) continue;
206
- closed[currentIdx] = 1;
207
- explored++;
208
- const cx = currentIdx % w;
209
- const cy = currentIdx / w | 0;
210
- const currentG = g[currentIdx];
211
- for (let d = 0; d < dirCount; d++) {
212
- const nx = cx + dirX[d];
213
- const ny = cy + dirY[d];
214
- if (nx < 0 || ny < 0 || nx >= w || ny >= grid.height) continue;
215
- const nIdx = ny * w + nx;
216
- if (closed[nIdx]) continue;
217
- const tileCost = grid.data[nIdx];
218
- if (tileCost === 0) continue;
219
- if (diagonal && d & 1) {
220
- const dx = dirX[d];
221
- const dy = dirY[d];
222
- if (!isWalkable(grid, cx + dx, cy) || !isWalkable(grid, cx, cy + dy)) continue;
223
- }
224
- const tentativeG = currentG + (dirCost ? dirCost[d] * tileCost : tileCost);
225
- if (tentativeG < g[nIdx]) {
226
- g[nIdx] = tentativeG;
227
- cameFrom[nIdx] = currentIdx;
228
- heapPush$1(heap, tentativeG + heuristic(nx, ny, ex, ey), nIdx);
242
+ try {
243
+ heapPush(heap, heuristic(sx, sy, ex, ey), startIdx);
244
+ while (heap.size > 0 && explored < maxIter) {
245
+ const currentIdx = heapPop(heap);
246
+ if (currentIdx === endIdx) return reconstructPath(cameFrom, w, currentIdx, g[currentIdx], explored);
247
+ if (stamp[currentIdx] === closedMark) continue;
248
+ stamp[currentIdx] = closedMark;
249
+ explored++;
250
+ const cx = currentIdx % w;
251
+ const cy = currentIdx / w | 0;
252
+ const currentG = g[currentIdx];
253
+ for (let d = 0; d < dirCount; d++) {
254
+ const nx = cx + dirX[d];
255
+ const ny = cy + dirY[d];
256
+ if (nx < 0 || ny < 0 || nx >= w || ny >= grid.height) continue;
257
+ const nIdx = ny * w + nx;
258
+ const nStamp = stamp[nIdx];
259
+ if (nStamp === closedMark) continue;
260
+ const tileCost = grid.data[nIdx];
261
+ if (tileCost === 0) continue;
262
+ if (diagonal && d & 1) {
263
+ const dx = dirX[d];
264
+ const dy = dirY[d];
265
+ if (!isWalkable(grid, cx + dx, cy) || !isWalkable(grid, cx, cy + dy)) continue;
266
+ }
267
+ const tentativeG = currentG + (dirCost ? dirCost[d] * tileCost : tileCost);
268
+ if (nStamp !== openMark || tentativeG < g[nIdx]) {
269
+ g[nIdx] = tentativeG;
270
+ cameFrom[nIdx] = currentIdx;
271
+ stamp[nIdx] = openMark;
272
+ heapPush(heap, tentativeG + heuristic(nx, ny, ex, ey), nIdx);
273
+ }
229
274
  }
230
275
  }
276
+ return null;
277
+ } finally {
278
+ releaseScratch(scratch);
231
279
  }
232
- return null;
233
280
  }
234
281
  function reconstructPath(cameFrom, w, endIdx, cost, explored) {
235
282
  const path = [];
@@ -249,74 +296,6 @@ function reconstructPath(cameFrom, w, endIdx, cost, explored) {
249
296
  };
250
297
  }
251
298
  // src/jps.ts
252
- function heapCreate(capacity) {
253
- return {
254
- f: new Float64Array(capacity),
255
- idx: new Int32Array(capacity),
256
- size: 0
257
- };
258
- }
259
- /**
260
- * Double the heap's backing arrays.
261
- *
262
- * Typed arrays do not throw on an out-of-range write — the store is silently
263
- * dropped — so without this a push past capacity increments `size`, writes
264
- * nothing, and every later comparison reads `undefined`. The open set then
265
- * degrades without any error, and the search returns a valid-looking but
266
- * non-optimal path. Growth is amortized O(1): allocation happens only when
267
- * capacity is reached, never per push.
268
- */
269
- function heapGrow(h) {
270
- const next = h.f.length === 0 ? 16 : h.f.length * 2;
271
- const f = new Float64Array(next);
272
- f.set(h.f);
273
- const idx = new Int32Array(next);
274
- idx.set(h.idx);
275
- h.f = f;
276
- h.idx = idx;
277
- }
278
- function heapPush(h, f, nodeIdx) {
279
- if (h.size === h.f.length) heapGrow(h);
280
- let i = h.size++;
281
- h.f[i] = f;
282
- h.idx[i] = nodeIdx;
283
- while (i > 0) {
284
- const parent = i - 1 >> 1;
285
- if (h.f[parent] <= h.f[i]) break;
286
- const tf = h.f[parent];
287
- const ti = h.idx[parent];
288
- h.f[parent] = h.f[i];
289
- h.idx[parent] = h.idx[i];
290
- h.f[i] = tf;
291
- h.idx[i] = ti;
292
- i = parent;
293
- }
294
- }
295
- function heapPop(h) {
296
- const result = h.idx[0];
297
- h.size--;
298
- if (h.size > 0) {
299
- h.f[0] = h.f[h.size];
300
- h.idx[0] = h.idx[h.size];
301
- let i = 0;
302
- for (;;) {
303
- let smallest = i;
304
- const left = 2 * i + 1;
305
- const right = 2 * i + 2;
306
- if (left < h.size && h.f[left] < h.f[smallest]) smallest = left;
307
- if (right < h.size && h.f[right] < h.f[smallest]) smallest = right;
308
- if (smallest === i) break;
309
- const tf = h.f[i];
310
- const ti = h.idx[i];
311
- h.f[i] = h.f[smallest];
312
- h.idx[i] = h.idx[smallest];
313
- h.f[smallest] = tf;
314
- h.idx[smallest] = ti;
315
- i = smallest;
316
- }
317
- }
318
- return result;
319
- }
320
299
  const SQRT2 = Math.SQRT2;
321
300
  function hasWeightedCost(grid) {
322
301
  const data = grid.data;
@@ -343,56 +322,66 @@ function findPathJPS(grid, startX, startY, endX, endY, options) {
343
322
  });
344
323
  const w = grid.width;
345
324
  const h = grid.height;
346
- const total = w * h;
347
- const maxIter = options?.maxIterations ?? total;
325
+ const maxIter = options?.maxIterations ?? w * h;
348
326
  const heuristic = options?.heuristic ?? octileDistance;
349
- const g = new Float64Array(total);
350
- g.fill(Number.POSITIVE_INFINITY);
351
- const closed = new Uint8Array(total);
352
- const cameFrom = new Int32Array(total);
353
- cameFrom.fill(-1);
354
327
  const startIdx = sy * w + sx;
355
328
  const endIdx = ey * w + ex;
329
+ const scratch = acquireScratch(grid);
330
+ const { g, cameFrom, stamp, heap, succ } = scratch;
331
+ const openMark = scratch.open;
332
+ const closedMark = openMark + 1;
333
+ stamp[startIdx] = openMark;
356
334
  g[startIdx] = 0;
357
- const heap = heapCreate(Math.min(total, 4096));
358
- heapPush(heap, heuristic(sx, sy, ex, ey), startIdx);
335
+ cameFrom[startIdx] = -1;
359
336
  let explored = 0;
360
- while (heap.size > 0 && explored < maxIter) {
361
- const currentIdx = heapPop(heap);
362
- if (currentIdx === endIdx) return buildPath(cameFrom, w, currentIdx, g[currentIdx], explored);
363
- if (closed[currentIdx]) continue;
364
- closed[currentIdx] = 1;
365
- explored++;
366
- const cx = currentIdx % w;
367
- const cy = currentIdx / w | 0;
368
- const neighbors = identifySuccessors(grid, cx, cy, cameFrom, w, h, ex, ey);
369
- for (let i = 0; i < neighbors.length; i += 2) {
370
- const nx = neighbors[i];
371
- const ny = neighbors[i + 1];
372
- const nIdx = ny * w + nx;
373
- if (closed[nIdx]) continue;
374
- const dx = Math.abs(nx - cx);
375
- const dy = Math.abs(ny - cy);
376
- const diag = Math.min(dx, dy);
377
- const straight = Math.abs(dx - dy);
378
- const dist = diag * SQRT2 + straight;
379
- const tentativeG = g[currentIdx] + dist;
380
- if (tentativeG < g[nIdx]) {
381
- g[nIdx] = tentativeG;
382
- cameFrom[nIdx] = currentIdx;
383
- heapPush(heap, tentativeG + heuristic(nx, ny, ex, ey), nIdx);
337
+ try {
338
+ heapPush(heap, heuristic(sx, sy, ex, ey), startIdx);
339
+ while (heap.size > 0 && explored < maxIter) {
340
+ const currentIdx = heapPop(heap);
341
+ if (currentIdx === endIdx) return buildPath(cameFrom, w, currentIdx, g[currentIdx], explored);
342
+ if (stamp[currentIdx] === closedMark) continue;
343
+ stamp[currentIdx] = closedMark;
344
+ explored++;
345
+ const cx = currentIdx % w;
346
+ const cy = currentIdx / w | 0;
347
+ const count = identifySuccessors(grid, cx, cy, cameFrom, w, h, ex, ey, succ);
348
+ for (let i = 0; i < count; i++) {
349
+ const nIdx = succ[i];
350
+ const nx = nIdx % w;
351
+ const ny = nIdx / w | 0;
352
+ const nStamp = stamp[nIdx];
353
+ if (nStamp === closedMark) continue;
354
+ const dx = Math.abs(nx - cx);
355
+ const dy = Math.abs(ny - cy);
356
+ const diag = Math.min(dx, dy);
357
+ const straight = Math.abs(dx - dy);
358
+ const dist = diag * SQRT2 + straight;
359
+ const tentativeG = g[currentIdx] + dist;
360
+ if (nStamp !== openMark || tentativeG < g[nIdx]) {
361
+ g[nIdx] = tentativeG;
362
+ cameFrom[nIdx] = currentIdx;
363
+ stamp[nIdx] = openMark;
364
+ heapPush(heap, tentativeG + heuristic(nx, ny, ex, ey), nIdx);
365
+ }
384
366
  }
385
367
  }
368
+ return null;
369
+ } finally {
370
+ releaseScratch(scratch);
386
371
  }
387
- return null;
388
372
  }
389
- function identifySuccessors(grid, cx, cy, cameFrom, w, h, ex, ey) {
390
- const results = [];
373
+ /**
374
+ * Writes the cell index of each jump point reachable from (cx, cy) into `out`
375
+ * and returns how many. At most one per direction tried — 8 from the start
376
+ * node, 5 after a diagonal move, 3 after a cardinal one — so `out` needs 8 slots.
377
+ */
378
+ function identifySuccessors(grid, cx, cy, cameFrom, w, h, ex, ey, out) {
379
+ let n = 0;
391
380
  const parentIdx = cameFrom[cy * w + cx];
392
381
  if (parentIdx === -1) for (let dx = -1; dx <= 1; dx++) for (let dy = -1; dy <= 1; dy++) {
393
382
  if (dx === 0 && dy === 0) continue;
394
383
  const jp = jump(grid, cx, cy, dx, dy, w, h, ex, ey);
395
- if (jp !== -1) results.push(jp % w, jp / w | 0);
384
+ if (jp !== -1) out[n++] = jp;
396
385
  }
397
386
  else {
398
387
  const px = parentIdx % w;
@@ -402,67 +391,70 @@ function identifySuccessors(grid, cx, cy, cameFrom, w, h, ex, ey) {
402
391
  if (dx !== 0 && dy !== 0) {
403
392
  if (isWalkable(grid, cx, cy + dy)) {
404
393
  const jp = jump(grid, cx, cy, 0, dy, w, h, ex, ey);
405
- if (jp !== -1) results.push(jp % w, jp / w | 0);
394
+ if (jp !== -1) out[n++] = jp;
406
395
  }
407
396
  if (isWalkable(grid, cx + dx, cy)) {
408
397
  const jp = jump(grid, cx, cy, dx, 0, w, h, ex, ey);
409
- if (jp !== -1) results.push(jp % w, jp / w | 0);
398
+ if (jp !== -1) out[n++] = jp;
410
399
  }
411
400
  if (isWalkable(grid, cx + dx, cy + dy)) {
412
401
  const jp = jump(grid, cx, cy, dx, dy, w, h, ex, ey);
413
- if (jp !== -1) results.push(jp % w, jp / w | 0);
402
+ if (jp !== -1) out[n++] = jp;
414
403
  }
415
404
  if (!isWalkable(grid, cx - dx, cy) && isWalkable(grid, cx - dx, cy + dy)) {
416
405
  const jp = jump(grid, cx, cy, -dx, dy, w, h, ex, ey);
417
- if (jp !== -1) results.push(jp % w, jp / w | 0);
406
+ if (jp !== -1) out[n++] = jp;
418
407
  }
419
408
  if (!isWalkable(grid, cx, cy - dy) && isWalkable(grid, cx + dx, cy - dy)) {
420
409
  const jp = jump(grid, cx, cy, dx, -dy, w, h, ex, ey);
421
- if (jp !== -1) results.push(jp % w, jp / w | 0);
410
+ if (jp !== -1) out[n++] = jp;
422
411
  }
423
412
  } else if (dx !== 0) {
424
413
  if (isWalkable(grid, cx + dx, cy)) {
425
414
  const jp = jump(grid, cx, cy, dx, 0, w, h, ex, ey);
426
- if (jp !== -1) results.push(jp % w, jp / w | 0);
415
+ if (jp !== -1) out[n++] = jp;
427
416
  }
428
417
  if (!isWalkable(grid, cx, cy + 1) && isWalkable(grid, cx + dx, cy + 1)) {
429
418
  const jp = jump(grid, cx, cy, dx, 1, w, h, ex, ey);
430
- if (jp !== -1) results.push(jp % w, jp / w | 0);
419
+ if (jp !== -1) out[n++] = jp;
431
420
  }
432
421
  if (!isWalkable(grid, cx, cy - 1) && isWalkable(grid, cx + dx, cy - 1)) {
433
422
  const jp = jump(grid, cx, cy, dx, -1, w, h, ex, ey);
434
- if (jp !== -1) results.push(jp % w, jp / w | 0);
423
+ if (jp !== -1) out[n++] = jp;
435
424
  }
436
425
  } else {
437
426
  if (isWalkable(grid, cx, cy + dy)) {
438
427
  const jp = jump(grid, cx, cy, 0, dy, w, h, ex, ey);
439
- if (jp !== -1) results.push(jp % w, jp / w | 0);
428
+ if (jp !== -1) out[n++] = jp;
440
429
  }
441
430
  if (!isWalkable(grid, cx + 1, cy) && isWalkable(grid, cx + 1, cy + dy)) {
442
431
  const jp = jump(grid, cx, cy, 1, dy, w, h, ex, ey);
443
- if (jp !== -1) results.push(jp % w, jp / w | 0);
432
+ if (jp !== -1) out[n++] = jp;
444
433
  }
445
434
  if (!isWalkable(grid, cx - 1, cy) && isWalkable(grid, cx - 1, cy + dy)) {
446
435
  const jp = jump(grid, cx, cy, -1, dy, w, h, ex, ey);
447
- if (jp !== -1) results.push(jp % w, jp / w | 0);
436
+ if (jp !== -1) out[n++] = jp;
448
437
  }
449
438
  }
450
439
  }
451
- return results;
440
+ return n;
452
441
  }
453
442
  function jump(grid, cx, cy, dx, dy, w, h, ex, ey) {
454
- const nx = cx + dx;
455
- const ny = cy + dy;
456
- if (!isWalkable(grid, nx, ny)) return -1;
457
- if (nx === ex && ny === ey) return ny * w + nx;
458
- if (dx !== 0 && dy !== 0) {
459
- if (!isWalkable(grid, nx - dx, ny) && isWalkable(grid, nx - dx, ny + dy) || !isWalkable(grid, nx, ny - dy) && isWalkable(grid, nx + dx, ny - dy)) return ny * w + nx;
460
- if (jump(grid, nx, ny, dx, 0, w, h, ex, ey) !== -1) return ny * w + nx;
461
- if (jump(grid, nx, ny, 0, dy, w, h, ex, ey) !== -1) return ny * w + nx;
462
- } else if (dx !== 0) {
463
- if (!isWalkable(grid, nx, ny + 1) && isWalkable(grid, nx + dx, ny + 1) || !isWalkable(grid, nx, ny - 1) && isWalkable(grid, nx + dx, ny - 1)) return ny * w + nx;
464
- } else if (!isWalkable(grid, nx + 1, ny) && isWalkable(grid, nx + 1, ny + dy) || !isWalkable(grid, nx - 1, ny) && isWalkable(grid, nx - 1, ny + dy)) return ny * w + nx;
465
- return jump(grid, nx, ny, dx, dy, w, h, ex, ey);
443
+ let nx = cx;
444
+ let ny = cy;
445
+ for (;;) {
446
+ nx += dx;
447
+ ny += dy;
448
+ if (!isWalkable(grid, nx, ny)) return -1;
449
+ if (nx === ex && ny === ey) return ny * w + nx;
450
+ if (dx !== 0 && dy !== 0) {
451
+ if (!isWalkable(grid, nx - dx, ny) && isWalkable(grid, nx - dx, ny + dy) || !isWalkable(grid, nx, ny - dy) && isWalkable(grid, nx + dx, ny - dy)) return ny * w + nx;
452
+ if (jump(grid, nx, ny, dx, 0, w, h, ex, ey) !== -1) return ny * w + nx;
453
+ if (jump(grid, nx, ny, 0, dy, w, h, ex, ey) !== -1) return ny * w + nx;
454
+ } else if (dx !== 0) {
455
+ if (!isWalkable(grid, nx, ny + 1) && isWalkable(grid, nx + dx, ny + 1) || !isWalkable(grid, nx, ny - 1) && isWalkable(grid, nx + dx, ny - 1)) return ny * w + nx;
456
+ } else if (!isWalkable(grid, nx + 1, ny) && isWalkable(grid, nx + 1, ny + dy) || !isWalkable(grid, nx - 1, ny) && isWalkable(grid, nx - 1, ny + dy)) return ny * w + nx;
457
+ }
466
458
  }
467
459
  function clampDir(v) {
468
460
  return v > 0 ? 1 : v < 0 ? -1 : 0;
@@ -530,18 +522,56 @@ function findPolygonAt(mesh, x, y) {
530
522
  for (let i = 0; i < mesh.polygons.length; i++) if (pointInConvexPolygon(mesh.polygons[i].vertices, x, y)) return mesh.polygons[i].id;
531
523
  return -1;
532
524
  }
533
- function centroid(verts) {
534
- let cx = 0;
535
- let cy = 0;
536
- for (let i = 0; i < verts.length; i++) {
537
- cx += verts[i].x;
538
- cy += verts[i].y;
525
+ const MAX_GEN = 4294967295;
526
+ const indices = /* @__PURE__ */ new WeakMap();
527
+ /** True when `pos` is exactly the id → position map of the live polygon list. */
528
+ function isCurrent(index, polygons) {
529
+ if (index.pos.size !== polygons.length) return false;
530
+ for (let i = 0; i < polygons.length; i++) if (index.pos.get(polygons[i].id) !== i) return false;
531
+ return true;
532
+ }
533
+ function rebuild(index, polygons) {
534
+ index.pos.clear();
535
+ for (let i = 0; i < polygons.length; i++) index.pos.set(polygons[i].id, i);
536
+ if (index.cx.length < polygons.length) {
537
+ index.cx = new Float64Array(polygons.length);
538
+ index.cy = new Float64Array(polygons.length);
539
+ index.stamp = new Uint32Array(polygons.length);
539
540
  }
540
- const n = verts.length;
541
- return {
542
- x: cx / n,
543
- y: cy / n
544
- };
541
+ }
542
+ /** The mesh's index, validated against the live polygon list and advanced to a new search generation. */
543
+ function acquireIndex(mesh) {
544
+ let index = indices.get(mesh);
545
+ if (index === void 0) {
546
+ index = {
547
+ pos: /* @__PURE__ */ new Map(),
548
+ cx: /* @__PURE__ */ new Float64Array(0),
549
+ cy: /* @__PURE__ */ new Float64Array(0),
550
+ stamp: /* @__PURE__ */ new Uint32Array(0),
551
+ gen: 0
552
+ };
553
+ indices.set(mesh, index);
554
+ }
555
+ if (!isCurrent(index, mesh.polygons)) rebuild(index, mesh.polygons);
556
+ if (index.gen === MAX_GEN) {
557
+ index.stamp.fill(0);
558
+ index.gen = 1;
559
+ } else index.gen++;
560
+ return index;
561
+ }
562
+ /** Makes `cx[i]` / `cy[i]` the centroid of the live polygon at `i`, summing its vertices once per search. */
563
+ function centroidAt(index, polygons, i) {
564
+ if (index.stamp[i] === index.gen) return;
565
+ const verts = polygons[i].vertices;
566
+ let sx = 0;
567
+ let sy = 0;
568
+ for (let j = 0; j < verts.length; j++) {
569
+ sx += verts[j].x;
570
+ sy += verts[j].y;
571
+ }
572
+ index.cx[i] = sx / verts.length;
573
+ index.cy[i] = sy / verts.length;
574
+ index.stamp[i] = index.gen;
545
575
  }
546
576
  function distSq(ax, ay, bx, by) {
547
577
  const dx = ax - bx;
@@ -551,6 +581,12 @@ function distSq(ax, ay, bx, by) {
551
581
  function dist(ax, ay, bx, by) {
552
582
  return Math.sqrt(distSq(ax, ay, bx, by));
553
583
  }
584
+ /**
585
+ * A* over the polygon graph, then funnel smoothing. Keeps an id index per mesh
586
+ * object (rebuilt when the polygon list changes) and computes each polygon's
587
+ * centroid at most once per search; edits to the mesh between searches are seen
588
+ * by the next one.
589
+ */
554
590
  function findNavMeshPath(mesh, start, end) {
555
591
  const startPolyId = findPolygonAt(mesh, start.x, start.y);
556
592
  const endPolyId = findPolygonAt(mesh, end.x, end.y);
@@ -565,18 +601,20 @@ function findNavMeshPath(mesh, start, end) {
565
601
  }],
566
602
  corridor: [startPolyId]
567
603
  };
568
- const byId = /* @__PURE__ */ new Map();
569
- for (let i = 0; i < mesh.polygons.length; i++) {
570
- const p = mesh.polygons[i];
571
- byId.set(p.id, p);
572
- }
604
+ const polygons = mesh.polygons;
605
+ const index = acquireIndex(mesh);
606
+ const { pos, cx, cy } = index;
573
607
  const gScore = /* @__PURE__ */ new Map();
574
608
  const fScore = /* @__PURE__ */ new Map();
575
609
  const cameFrom = /* @__PURE__ */ new Map();
576
610
  gScore.set(startPolyId, 0);
577
- const endCenter = centroid(byId.get(endPolyId).vertices);
578
- const startCenter = centroid(byId.get(startPolyId).vertices);
579
- fScore.set(startPolyId, dist(startCenter.x, startCenter.y, endCenter.x, endCenter.y));
611
+ const endPos = pos.get(endPolyId);
612
+ centroidAt(index, polygons, endPos);
613
+ const endCx = cx[endPos];
614
+ const endCy = cy[endPos];
615
+ const startPos = pos.get(startPolyId);
616
+ centroidAt(index, polygons, startPos);
617
+ fScore.set(startPolyId, dist(cx[startPos], cy[startPos], endCx, endCy));
580
618
  const open = [startPolyId];
581
619
  const inOpen = /* @__PURE__ */ new Set([startPolyId]);
582
620
  const closed = /* @__PURE__ */ new Set();
@@ -600,7 +638,7 @@ function findNavMeshPath(mesh, start, end) {
600
638
  }
601
639
  corridor.reverse();
602
640
  return {
603
- path: funnelSmooth(mesh, byId, corridor, start, end),
641
+ path: funnelSmooth(mesh, index, corridor, start, end),
604
642
  corridor
605
643
  };
606
644
  }
@@ -608,24 +646,25 @@ function findNavMeshPath(mesh, start, end) {
608
646
  open.length--;
609
647
  inOpen.delete(currentId);
610
648
  closed.add(currentId);
611
- const current = byId.get(currentId);
612
- const currentCenter = centroid(current.vertices);
649
+ const currentPos = pos.get(currentId);
650
+ const current = polygons[currentPos];
651
+ centroidAt(index, polygons, currentPos);
652
+ const currentCx = cx[currentPos];
653
+ const currentCy = cy[currentPos];
613
654
  const currentG = gScore.get(currentId);
614
655
  for (let i = 0; i < current.portals.length; i++) {
615
656
  const portal = current.portals[i];
616
657
  const neighborId = portal.neighborId;
617
658
  if (closed.has(neighborId)) continue;
618
- const neighbor = byId.get(neighborId);
619
- if (!neighbor) continue;
620
- const portalMidX = (portal.a.x + portal.b.x) * .5;
621
- const portalMidY = (portal.a.y + portal.b.y) * .5;
622
- const tentativeG = currentG + dist(currentCenter.x, currentCenter.y, portalMidX, portalMidY);
659
+ const neighborPos = pos.get(neighborId);
660
+ if (neighborPos === void 0) continue;
661
+ const tentativeG = currentG + dist(currentCx, currentCy, (portal.a.x + portal.b.x) * .5, (portal.a.y + portal.b.y) * .5);
623
662
  const prevG = gScore.get(neighborId);
624
663
  if (prevG !== void 0 && tentativeG >= prevG) continue;
625
664
  gScore.set(neighborId, tentativeG);
626
665
  cameFrom.set(neighborId, currentId);
627
- const neighborCenter = centroid(neighbor.vertices);
628
- fScore.set(neighborId, tentativeG + dist(neighborCenter.x, neighborCenter.y, endCenter.x, endCenter.y));
666
+ centroidAt(index, polygons, neighborPos);
667
+ fScore.set(neighborId, tentativeG + dist(cx[neighborPos], cy[neighborPos], endCx, endCy));
629
668
  if (!inOpen.has(neighborId)) {
630
669
  open.push(neighborId);
631
670
  inOpen.add(neighborId);
@@ -634,7 +673,7 @@ function findNavMeshPath(mesh, start, end) {
634
673
  }
635
674
  return null;
636
675
  }
637
- function funnelSmooth(_mesh, byId, corridor, start, end) {
676
+ function funnelSmooth(mesh, index, corridor, start, end) {
638
677
  if (corridor.length <= 1) return [{
639
678
  x: start.x,
640
679
  y: start.y
@@ -653,7 +692,8 @@ function funnelSmooth(_mesh, byId, corridor, start, end) {
653
692
  y: start.y
654
693
  });
655
694
  for (let i = 0; i < corridor.length - 1; i++) {
656
- const poly = byId.get(corridor[i]);
695
+ const polyPos = index.pos.get(corridor[i]);
696
+ const poly = mesh.polygons[polyPos];
657
697
  const nextId = corridor[i + 1];
658
698
  let found = false;
659
699
  for (let j = 0; j < poly.portals.length; j++) if (poly.portals[j].neighborId === nextId) {
@@ -663,7 +703,11 @@ function funnelSmooth(_mesh, byId, corridor, start, end) {
663
703
  break;
664
704
  }
665
705
  if (!found) {
666
- const c = centroid(poly.vertices);
706
+ centroidAt(index, mesh.polygons, polyPos);
707
+ const c = {
708
+ x: index.cx[polyPos],
709
+ y: index.cy[polyPos]
710
+ };
667
711
  portalsLeft.push(c);
668
712
  portalsRight.push(c);
669
713
  }