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