@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.d.mts +6 -0
- package/dist/index.d.mts.map +1 -1
- package/dist/index.d.ts +6 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +264 -220
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +264 -220
- package/dist/index.mjs.map +1 -1
- package/package.json +2 -2
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/
|
|
41
|
-
|
|
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
|
|
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
|
|
85
|
-
if (h.size === h.f.length) heapGrow
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
const
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
const
|
|
221
|
-
|
|
222
|
-
if (
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
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
|
|
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
|
-
|
|
358
|
-
heapPush(heap, heuristic(sx, sy, ex, ey), startIdx);
|
|
335
|
+
cameFrom[startIdx] = -1;
|
|
359
336
|
let explored = 0;
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
const
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
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
|
-
|
|
390
|
-
|
|
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)
|
|
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)
|
|
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)
|
|
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)
|
|
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)
|
|
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)
|
|
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)
|
|
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)
|
|
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)
|
|
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)
|
|
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)
|
|
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)
|
|
436
|
+
if (jp !== -1) out[n++] = jp;
|
|
448
437
|
}
|
|
449
438
|
}
|
|
450
439
|
}
|
|
451
|
-
return
|
|
440
|
+
return n;
|
|
452
441
|
}
|
|
453
442
|
function jump(grid, cx, cy, dx, dy, w, h, ex, ey) {
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
if (!isWalkable(grid, nx
|
|
460
|
-
if (
|
|
461
|
-
if (
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
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
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
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
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
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
|
|
569
|
-
|
|
570
|
-
|
|
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
|
|
578
|
-
|
|
579
|
-
|
|
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,
|
|
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
|
|
612
|
-
const
|
|
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
|
|
619
|
-
if (
|
|
620
|
-
const
|
|
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
|
-
|
|
628
|
-
fScore.set(neighborId, tentativeG + dist(
|
|
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(
|
|
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
|
|
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
|
-
|
|
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
|
}
|