@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.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/
|
|
42
|
-
|
|
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
|
|
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
|
|
86
|
-
if (h.size === h.f.length) heapGrow
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
const
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
const
|
|
222
|
-
|
|
223
|
-
if (
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
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
|
|
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
|
-
|
|
359
|
-
heapPush(heap, heuristic(sx, sy, ex, ey), startIdx);
|
|
336
|
+
cameFrom[startIdx] = -1;
|
|
360
337
|
let explored = 0;
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
const
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
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
|
-
|
|
391
|
-
|
|
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)
|
|
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)
|
|
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)
|
|
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)
|
|
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)
|
|
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)
|
|
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)
|
|
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)
|
|
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)
|
|
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)
|
|
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)
|
|
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)
|
|
437
|
+
if (jp !== -1) out[n++] = jp;
|
|
449
438
|
}
|
|
450
439
|
}
|
|
451
440
|
}
|
|
452
|
-
return
|
|
441
|
+
return n;
|
|
453
442
|
}
|
|
454
443
|
function jump(grid, cx, cy, dx, dy, w, h, ex, ey) {
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
if (!isWalkable(grid, nx
|
|
461
|
-
if (
|
|
462
|
-
if (
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
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
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
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
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
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
|
|
570
|
-
|
|
571
|
-
|
|
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
|
|
579
|
-
|
|
580
|
-
|
|
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,
|
|
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
|
|
613
|
-
const
|
|
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
|
|
620
|
-
if (
|
|
621
|
-
const
|
|
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
|
-
|
|
629
|
-
fScore.set(neighborId, tentativeG + dist(
|
|
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(
|
|
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
|
|
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
|
-
|
|
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
|
}
|