@woosh/meep-engine 2.76.0 → 2.76.1
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/build/meep.cjs +4 -2
- package/build/meep.module.js +4 -2
- package/package.json +1 -1
- package/src/core/binary/BitSet.js +4 -2
- package/src/core/collection/heap/Uin32Heap.spec.js +14 -1
- package/src/core/collection/heap/Uint32Heap.js +31 -16
- package/src/engine/navigation/grid/find_path_on_grid_astar.js +193 -124
- package/src/engine/navigation/grid/find_path_on_grid_astar.spec.js +75 -15
package/build/meep.cjs
CHANGED
|
@@ -71805,7 +71805,7 @@ BitSet.prototype.shift = function (distance, start_index, end_index) {
|
|
|
71805
71805
|
};
|
|
71806
71806
|
|
|
71807
71807
|
/**
|
|
71808
|
-
* Sets all
|
|
71808
|
+
* Sets all the bits in this BitSet to false.
|
|
71809
71809
|
*/
|
|
71810
71810
|
BitSet.prototype.reset = function () {
|
|
71811
71811
|
const current_length = this.__length;
|
|
@@ -71813,7 +71813,9 @@ BitSet.prototype.reset = function () {
|
|
|
71813
71813
|
if (current_length <= 0) {
|
|
71814
71814
|
// no action required
|
|
71815
71815
|
return;
|
|
71816
|
-
}
|
|
71816
|
+
}
|
|
71817
|
+
|
|
71818
|
+
if (current_length <= 32) {
|
|
71817
71819
|
// one word, set first word to 0 manually
|
|
71818
71820
|
this.__data_uint32[0] = 0;
|
|
71819
71821
|
} else {
|
package/build/meep.module.js
CHANGED
|
@@ -71803,7 +71803,7 @@ BitSet.prototype.shift = function (distance, start_index, end_index) {
|
|
|
71803
71803
|
};
|
|
71804
71804
|
|
|
71805
71805
|
/**
|
|
71806
|
-
* Sets all
|
|
71806
|
+
* Sets all the bits in this BitSet to false.
|
|
71807
71807
|
*/
|
|
71808
71808
|
BitSet.prototype.reset = function () {
|
|
71809
71809
|
const current_length = this.__length;
|
|
@@ -71811,7 +71811,9 @@ BitSet.prototype.reset = function () {
|
|
|
71811
71811
|
if (current_length <= 0) {
|
|
71812
71812
|
// no action required
|
|
71813
71813
|
return;
|
|
71814
|
-
}
|
|
71814
|
+
}
|
|
71815
|
+
|
|
71816
|
+
if (current_length <= 32) {
|
|
71815
71817
|
// one word, set first word to 0 manually
|
|
71816
71818
|
this.__data_uint32[0] = 0;
|
|
71817
71819
|
} else {
|
package/package.json
CHANGED
|
@@ -546,7 +546,7 @@ BitSet.prototype.shift = function (distance, start_index, end_index) {
|
|
|
546
546
|
};
|
|
547
547
|
|
|
548
548
|
/**
|
|
549
|
-
* Sets all
|
|
549
|
+
* Sets all the bits in this BitSet to false.
|
|
550
550
|
*/
|
|
551
551
|
BitSet.prototype.reset = function () {
|
|
552
552
|
const current_length = this.__length;
|
|
@@ -554,7 +554,9 @@ BitSet.prototype.reset = function () {
|
|
|
554
554
|
if (current_length <= 0) {
|
|
555
555
|
// no action required
|
|
556
556
|
return;
|
|
557
|
-
}
|
|
557
|
+
}
|
|
558
|
+
|
|
559
|
+
if (current_length <= 32) {
|
|
558
560
|
// one word, set first word to 0 manually
|
|
559
561
|
this.__data_uint32[0] = 0;
|
|
560
562
|
} else {
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { Uint32Heap } from "./Uint32Heap.js";
|
|
2
1
|
import { seededRandom } from "../../math/random/seededRandom.js";
|
|
2
|
+
import { Uint32Heap } from "./Uint32Heap.js";
|
|
3
3
|
|
|
4
4
|
test('initial capacity setting via constructor', () => {
|
|
5
5
|
const heap = new Uint32Heap(3);
|
|
@@ -126,6 +126,19 @@ test("insert_or_update", () => {
|
|
|
126
126
|
|
|
127
127
|
});
|
|
128
128
|
|
|
129
|
+
test("clear should make heap empty", () => {
|
|
130
|
+
const heap = new Uint32Heap();
|
|
131
|
+
|
|
132
|
+
heap.clear();
|
|
133
|
+
|
|
134
|
+
expect(heap.is_empty()).toBe(true);
|
|
135
|
+
|
|
136
|
+
heap.insert(1, 7);
|
|
137
|
+
|
|
138
|
+
heap.clear();
|
|
139
|
+
|
|
140
|
+
expect(heap.is_empty()).toBe(true);
|
|
141
|
+
});
|
|
129
142
|
|
|
130
143
|
test.skip("performance 1,000,000 random element fill / drain", () => {
|
|
131
144
|
|
|
@@ -55,12 +55,12 @@ function HEAP_RIGHT(i) {
|
|
|
55
55
|
export class Uint32Heap {
|
|
56
56
|
/**
|
|
57
57
|
*
|
|
58
|
-
* @param {number} [
|
|
58
|
+
* @param {number} [initial_capacity] Can supply initial capacity, heap will still grow when necessary. This allows to prevent needless re-allocations when max heap size is known in advance
|
|
59
59
|
*/
|
|
60
|
-
constructor(
|
|
61
|
-
assert.isNonNegativeInteger(
|
|
60
|
+
constructor(initial_capacity = DEFAULT_CAPACITY) {
|
|
61
|
+
assert.isNonNegativeInteger(initial_capacity, 'capacity');
|
|
62
62
|
|
|
63
|
-
this.__data_buffer = new ArrayBuffer(
|
|
63
|
+
this.__data_buffer = new ArrayBuffer(initial_capacity * ELEMENT_BYTE_SIZE);
|
|
64
64
|
|
|
65
65
|
/**
|
|
66
66
|
* Used to access stored IDs
|
|
@@ -76,7 +76,7 @@ export class Uint32Heap {
|
|
|
76
76
|
*/
|
|
77
77
|
this.__data_float32 = new Float32Array(this.__data_buffer);
|
|
78
78
|
|
|
79
|
-
this.__capacity =
|
|
79
|
+
this.__capacity = initial_capacity;
|
|
80
80
|
this.__size = 0;
|
|
81
81
|
}
|
|
82
82
|
|
|
@@ -140,9 +140,12 @@ export class Uint32Heap {
|
|
|
140
140
|
uint32[i2] = uint32[j2];
|
|
141
141
|
uint32[j2] = mem_0;
|
|
142
142
|
|
|
143
|
-
const
|
|
144
|
-
|
|
145
|
-
|
|
143
|
+
const i21 = i2 + 1;
|
|
144
|
+
const j21 = j2 + 1;
|
|
145
|
+
|
|
146
|
+
const mem_1 = uint32[i21];
|
|
147
|
+
uint32[i21] = uint32[j21];
|
|
148
|
+
uint32[j21] = mem_1;
|
|
146
149
|
}
|
|
147
150
|
|
|
148
151
|
/**
|
|
@@ -263,7 +266,7 @@ export class Uint32Heap {
|
|
|
263
266
|
*/
|
|
264
267
|
find_index_by_id(id) {
|
|
265
268
|
const n = this.__size
|
|
266
|
-
const n2 = n *
|
|
269
|
+
const n2 = n << 1; // fast *2 multiplication
|
|
267
270
|
|
|
268
271
|
const uint32 = this.__data_uint32;
|
|
269
272
|
|
|
@@ -279,6 +282,12 @@ export class Uint32Heap {
|
|
|
279
282
|
return -1;
|
|
280
283
|
}
|
|
281
284
|
|
|
285
|
+
/**
|
|
286
|
+
* Clear out all the data, heap will be made empty
|
|
287
|
+
*/
|
|
288
|
+
clear() {
|
|
289
|
+
this.__size = 0;
|
|
290
|
+
}
|
|
282
291
|
|
|
283
292
|
/**
|
|
284
293
|
*
|
|
@@ -331,14 +340,16 @@ export class Uint32Heap {
|
|
|
331
340
|
}
|
|
332
341
|
|
|
333
342
|
/**
|
|
334
|
-
*
|
|
343
|
+
* Update score of an element referenced directly by index, this is a fast method, but you're generally not going to know the index so in most cases it's best to use "update_score" instead
|
|
335
344
|
* @param {number} index
|
|
336
345
|
* @param {number} score
|
|
337
346
|
*/
|
|
338
347
|
__update_score_by_index(index, score) {
|
|
339
348
|
|
|
340
349
|
const float32 = this.__data_float32;
|
|
341
|
-
|
|
350
|
+
|
|
351
|
+
const index2 = index << 1; // fast *2 multiplication
|
|
352
|
+
|
|
342
353
|
const existing_score = float32[index2];
|
|
343
354
|
|
|
344
355
|
if (score < existing_score) {
|
|
@@ -362,7 +373,9 @@ export class Uint32Heap {
|
|
|
362
373
|
return Number.NaN;
|
|
363
374
|
}
|
|
364
375
|
|
|
365
|
-
|
|
376
|
+
const index2 = index << 1; // fast *2 multiplication
|
|
377
|
+
|
|
378
|
+
return this.__data_float32[index2];
|
|
366
379
|
}
|
|
367
380
|
|
|
368
381
|
|
|
@@ -391,21 +404,23 @@ export class Uint32Heap {
|
|
|
391
404
|
assert.lessThanOrEqual(id, UINT32_MAX - 1, 'must be less than or equal to (2^32 - 2)');
|
|
392
405
|
assert.isNumber(score, 'score');
|
|
393
406
|
|
|
394
|
-
|
|
407
|
+
const current_size = this.__size;
|
|
408
|
+
|
|
409
|
+
if (current_size >= this.__capacity) {
|
|
395
410
|
// need to re-allocate
|
|
396
411
|
this.__capacity_grow();
|
|
397
412
|
}
|
|
398
413
|
|
|
399
414
|
// insert at the end
|
|
400
|
-
const index =
|
|
401
|
-
const address = index
|
|
415
|
+
const index = current_size;
|
|
416
|
+
const address = index << 1;
|
|
402
417
|
|
|
403
418
|
// write data
|
|
404
419
|
this.__data_float32[address] = score;
|
|
405
420
|
this.__data_uint32[address + 1] = id;
|
|
406
421
|
|
|
407
422
|
// record increased size
|
|
408
|
-
this.__size
|
|
423
|
+
this.__size = current_size + 1;
|
|
409
424
|
|
|
410
425
|
this.heap_up(index);
|
|
411
426
|
}
|
|
@@ -1,7 +1,9 @@
|
|
|
1
|
-
import BinaryHeap from '../../../core/collection/heap/FastBinaryHeap.js';
|
|
2
|
-
import Vector2 from '../../../core/geom/Vector2.js';
|
|
3
1
|
import { assert } from "../../../core/assert.js";
|
|
2
|
+
import { BitSet } from "../../../core/binary/BitSet.js";
|
|
3
|
+
import { Uint32Heap } from "../../../core/collection/heap/Uint32Heap.js";
|
|
4
|
+
import Vector2 from '../../../core/geom/Vector2.js';
|
|
4
5
|
|
|
6
|
+
const neighbors = new Uint32Array(4);
|
|
5
7
|
|
|
6
8
|
/**
|
|
7
9
|
*
|
|
@@ -18,28 +20,169 @@ function index2point(result, index, width) {
|
|
|
18
20
|
|
|
19
21
|
/**
|
|
20
22
|
*
|
|
21
|
-
* @param {number[]} result
|
|
23
|
+
* @param {number[]|Uint32Array} result
|
|
22
24
|
* @param {number} index
|
|
23
25
|
* @param {number} width
|
|
24
26
|
* @param {number} height
|
|
27
|
+
* @returns {number}
|
|
25
28
|
*/
|
|
26
|
-
function
|
|
29
|
+
function computeNeighbors(result, index, width, height) {
|
|
27
30
|
const x = index % width;
|
|
28
31
|
const y = (index / width) | 0;
|
|
32
|
+
|
|
33
|
+
let neighbour_count = 0;
|
|
34
|
+
|
|
35
|
+
if (y > 0) {
|
|
36
|
+
result[neighbour_count] = index - width;
|
|
37
|
+
neighbour_count++
|
|
38
|
+
}
|
|
29
39
|
if (x > 0) {
|
|
30
|
-
result
|
|
40
|
+
result[neighbour_count] = index - 1;
|
|
41
|
+
neighbour_count++;
|
|
31
42
|
}
|
|
32
43
|
if (x < width - 1) {
|
|
33
|
-
result
|
|
34
|
-
|
|
35
|
-
if (y > 0) {
|
|
36
|
-
result.push(index - width);
|
|
44
|
+
result[neighbour_count] = index + 1;
|
|
45
|
+
neighbour_count++;
|
|
37
46
|
}
|
|
38
47
|
if (y < height - 1) {
|
|
39
|
-
result
|
|
48
|
+
result[neighbour_count] = index + width;
|
|
49
|
+
neighbour_count++
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
return neighbour_count;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
*
|
|
57
|
+
* @param {number} node
|
|
58
|
+
* @param {number[]} g_score
|
|
59
|
+
* @param {number} width
|
|
60
|
+
* @param {number} height
|
|
61
|
+
* @returns {number[]}
|
|
62
|
+
*/
|
|
63
|
+
function pathTo(node, g_score, width, height) {
|
|
64
|
+
let v_previous = new Vector2(-1, -1);
|
|
65
|
+
let v_current = new Vector2();
|
|
66
|
+
|
|
67
|
+
let dx = 1,
|
|
68
|
+
dy = 1,
|
|
69
|
+
_dx = 0,
|
|
70
|
+
_dy = 0;
|
|
71
|
+
|
|
72
|
+
const result = [];
|
|
73
|
+
|
|
74
|
+
let current = node;
|
|
75
|
+
let prev = current;
|
|
76
|
+
|
|
77
|
+
let last_value = g_score[current];
|
|
78
|
+
|
|
79
|
+
while (current !== -1) {
|
|
80
|
+
index2point(v_current, current, width);
|
|
81
|
+
|
|
82
|
+
_dx = v_current.x - v_previous.x;
|
|
83
|
+
_dy = v_current.y - v_previous.y;
|
|
84
|
+
|
|
85
|
+
// normalize
|
|
86
|
+
if (_dx !== 0) {
|
|
87
|
+
_dx /= Math.abs(_dx);
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
if (_dy !== 0) {
|
|
91
|
+
_dy /= Math.abs(_dy);
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
const direction_change = dx !== _dx || dy !== _dy;
|
|
95
|
+
|
|
96
|
+
if (direction_change) {
|
|
97
|
+
// direction changed
|
|
98
|
+
dx = _dx;
|
|
99
|
+
dy = _dy;
|
|
100
|
+
|
|
101
|
+
//only record points where connection bends to save space
|
|
102
|
+
result.push(prev);
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
prev = current;
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
//swap vectors
|
|
109
|
+
const t = v_previous;
|
|
110
|
+
v_previous = v_current;
|
|
111
|
+
v_current = t;
|
|
112
|
+
|
|
113
|
+
// find next point
|
|
114
|
+
const neighbour_count = computeNeighbors(neighbors, current, width, height);
|
|
115
|
+
|
|
116
|
+
current = -1;
|
|
117
|
+
|
|
118
|
+
for (let i = 0; i < neighbour_count; i++) {
|
|
119
|
+
const neighbor = neighbors[i];
|
|
120
|
+
|
|
121
|
+
const score = g_score[neighbor];
|
|
122
|
+
|
|
123
|
+
if (score < last_value) {
|
|
124
|
+
current = neighbor;
|
|
125
|
+
last_value = score;
|
|
126
|
+
} else if (score === last_value) {
|
|
127
|
+
// same score, prefer non-bending neighbours
|
|
128
|
+
|
|
129
|
+
index2point(v_current, neighbor, width);
|
|
130
|
+
|
|
131
|
+
_dx = v_current.x - v_previous.x;
|
|
132
|
+
_dy = v_current.y - v_previous.y;
|
|
133
|
+
|
|
134
|
+
if (_dx === dx && _dy === dy) {
|
|
135
|
+
// same direction
|
|
136
|
+
current = neighbor;
|
|
137
|
+
last_value = score;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
|
|
40
143
|
}
|
|
144
|
+
|
|
145
|
+
if (result[result.length - 1] !== prev) {
|
|
146
|
+
|
|
147
|
+
//check if last node needs to be added
|
|
148
|
+
result.push(prev);
|
|
149
|
+
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
result.reverse();
|
|
153
|
+
|
|
154
|
+
return result;
|
|
41
155
|
}
|
|
42
156
|
|
|
157
|
+
|
|
158
|
+
/**
|
|
159
|
+
*
|
|
160
|
+
* @param {number} index0
|
|
161
|
+
* @param {number} index1
|
|
162
|
+
* @param {number} width
|
|
163
|
+
* @returns {number}
|
|
164
|
+
*/
|
|
165
|
+
function heuristic(index0, index1, width) {
|
|
166
|
+
const x1 = index0 % width;
|
|
167
|
+
const y1 = (index0 / width) | 0;
|
|
168
|
+
|
|
169
|
+
//
|
|
170
|
+
const x2 = index1 % width;
|
|
171
|
+
const y2 = (index1 / width) | 0;
|
|
172
|
+
|
|
173
|
+
//
|
|
174
|
+
const dx = Math.abs(x1 - x2);
|
|
175
|
+
const dy = Math.abs(y1 - y2);
|
|
176
|
+
|
|
177
|
+
return dx + dy;
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
|
|
181
|
+
const open = new Uint32Heap();
|
|
182
|
+
|
|
183
|
+
const closed = new BitSet();
|
|
184
|
+
closed.preventShrink();
|
|
185
|
+
|
|
43
186
|
/**
|
|
44
187
|
*
|
|
45
188
|
* @param {number[]|Uint8Array|Uint16Array|Float32Array} field
|
|
@@ -48,8 +191,7 @@ function computeOrthogonalNeighbors(result, index, width, height) {
|
|
|
48
191
|
* @param {number} start
|
|
49
192
|
* @param {number} goal
|
|
50
193
|
* @param {number} crossingPenalty
|
|
51
|
-
* @param {number}
|
|
52
|
-
* @param {number} blockValue
|
|
194
|
+
* @param {number} blockValue value in the field that signifies impassible obstacle
|
|
53
195
|
* @returns {Array.<number>} array of indices representing path from start to end
|
|
54
196
|
*/
|
|
55
197
|
export function find_path_on_grid_astar(
|
|
@@ -57,170 +199,97 @@ export function find_path_on_grid_astar(
|
|
|
57
199
|
width, height,
|
|
58
200
|
start, goal,
|
|
59
201
|
crossingPenalty,
|
|
60
|
-
bendPenalty,
|
|
61
202
|
blockValue
|
|
62
203
|
) {
|
|
63
204
|
|
|
64
205
|
assert.defined(field, 'field');
|
|
65
206
|
assert.notNull(field, 'field');
|
|
66
207
|
|
|
67
|
-
|
|
68
208
|
assert.isNonNegativeInteger(start, "start");
|
|
69
209
|
assert.isNonNegativeInteger(goal, "goal");
|
|
70
210
|
|
|
71
211
|
assert.isNumber(crossingPenalty, 'crossingPenalty');
|
|
72
|
-
assert.isNumber(bendPenalty, 'bendPenalty');
|
|
73
212
|
assert.isNumber(blockValue, 'blockValue');
|
|
74
213
|
|
|
214
|
+
// prepare sets
|
|
215
|
+
open.clear();
|
|
216
|
+
closed.reset();
|
|
75
217
|
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
const y1 = Math.floor(index0 / width);
|
|
81
|
-
//
|
|
82
|
-
const x2 = index1 % width;
|
|
83
|
-
const y2 = Math.floor(index1 / width);
|
|
84
|
-
//
|
|
85
|
-
const dx = Math.abs(x1 - x2);
|
|
86
|
-
const dy = Math.abs(y1 - y2);
|
|
87
|
-
return dx + dy;
|
|
88
|
-
};
|
|
89
|
-
|
|
90
|
-
const came_from = [];
|
|
91
|
-
const f_score = [];
|
|
218
|
+
/**
|
|
219
|
+
* Contains refined heuristic value
|
|
220
|
+
* @type {number[]}
|
|
221
|
+
*/
|
|
92
222
|
const g_score = [];
|
|
93
223
|
|
|
94
|
-
|
|
95
|
-
return f_score[i1];
|
|
96
|
-
});
|
|
224
|
+
g_score[start] = 0;
|
|
97
225
|
|
|
98
|
-
const closed = [];
|
|
99
226
|
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
const directionTo = [];
|
|
104
|
-
directionTo[start] = 0;
|
|
105
|
-
|
|
106
|
-
open.push(start);
|
|
107
|
-
|
|
108
|
-
function pathTo(node) {
|
|
109
|
-
let pP = new Vector2(-1, -1),
|
|
110
|
-
pC = new Vector2();
|
|
111
|
-
let dx = 1,
|
|
112
|
-
dy = 1,
|
|
113
|
-
_dx = 0,
|
|
114
|
-
_dy = 0;
|
|
115
|
-
const result = [];
|
|
116
|
-
let prev = node;
|
|
117
|
-
while (node !== void 0) {
|
|
118
|
-
index2point(pC, node, width);
|
|
119
|
-
_dx = pC.x - pP.x;
|
|
120
|
-
_dy = pC.y - pP.y;
|
|
121
|
-
//
|
|
122
|
-
if (_dx !== 0) {
|
|
123
|
-
_dx /= Math.abs(_dx);
|
|
124
|
-
}
|
|
125
|
-
if (_dy !== 0) {
|
|
126
|
-
_dy /= Math.abs(_dy);
|
|
127
|
-
}
|
|
128
|
-
if (dx !== _dx || dy !== _dy) {
|
|
129
|
-
dx = _dx;
|
|
130
|
-
dy = _dy;
|
|
131
|
-
//only record points where connection bends to save space
|
|
132
|
-
result.push(prev);
|
|
133
|
-
}
|
|
134
|
-
prev = node;
|
|
135
|
-
node = came_from[node];
|
|
136
|
-
//swap
|
|
137
|
-
const t = pP;
|
|
138
|
-
pP = pC;
|
|
139
|
-
pC = t;
|
|
140
|
-
}
|
|
141
|
-
if (result[result.length - 1] !== prev) {
|
|
142
|
-
//check if last node needs to be added
|
|
143
|
-
result.push(prev);
|
|
144
|
-
}
|
|
145
|
-
result.reverse();
|
|
146
|
-
return result;
|
|
147
|
-
}
|
|
227
|
+
open.insert(start, heuristic(start, goal, width));
|
|
228
|
+
|
|
229
|
+
while (!open.is_empty()) {
|
|
148
230
|
|
|
149
|
-
const neighbors = [];
|
|
150
|
-
while (open.size() > 0) {
|
|
151
|
-
if (limitCycles-- === 0) {
|
|
152
|
-
throw new Error("maximum number of cycles reached");
|
|
153
|
-
}
|
|
154
231
|
// Grab the lowest f(x) to process next. Heap keeps this sorted for us.
|
|
155
|
-
const currentNode = open.
|
|
232
|
+
const currentNode = open.pop_min();
|
|
156
233
|
|
|
157
|
-
// End case -- result has been found, return the traced path.
|
|
158
234
|
if (currentNode === goal) {
|
|
159
|
-
return
|
|
235
|
+
// End case -- result has been found, return the traced path.
|
|
236
|
+
return pathTo(currentNode, g_score, width, height);
|
|
160
237
|
}
|
|
161
238
|
|
|
162
239
|
// Normal case -- move currentNode from open to closed, process each of its neighbors.
|
|
163
|
-
closed
|
|
240
|
+
closed.set(currentNode, true);
|
|
164
241
|
|
|
165
242
|
// Find all neighbors for the current node.
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
if (numNeighbours === 0) {
|
|
170
|
-
continue;
|
|
171
|
-
}
|
|
172
|
-
const directionToCurrent = directionTo[currentNode];
|
|
173
|
-
for (let i = 0; i < numNeighbours; ++i) {
|
|
243
|
+
const neighbour_count = computeNeighbors(neighbors, currentNode, width, height);
|
|
244
|
+
|
|
245
|
+
for (let i = 0; i < neighbour_count; ++i) {
|
|
174
246
|
const neighbor = neighbors[i];
|
|
175
247
|
|
|
176
|
-
if (closed
|
|
177
|
-
//
|
|
248
|
+
if (closed.get(neighbor)) {
|
|
249
|
+
// Already closed, skip to next neighbor.
|
|
178
250
|
continue;
|
|
179
251
|
}
|
|
180
252
|
|
|
181
253
|
// The g score is the shortest distance from start to current node.
|
|
182
254
|
// We need to check if the path we have arrived at this neighbor is the shortest one we have seen yet.
|
|
183
255
|
const neighborValue = field[neighbor];
|
|
256
|
+
|
|
184
257
|
if (neighborValue === blockValue) {
|
|
185
|
-
//cell is blocked,
|
|
186
|
-
closed
|
|
258
|
+
//cell is blocked, close and continue
|
|
259
|
+
closed.set(neighbor, true);
|
|
187
260
|
continue;
|
|
188
261
|
}
|
|
189
|
-
const direction = neighbor - currentNode;
|
|
190
|
-
let turnValue;
|
|
191
|
-
if (direction !== directionToCurrent) {
|
|
192
|
-
turnValue = bendPenalty;
|
|
193
|
-
} else {
|
|
194
|
-
turnValue = 0;
|
|
195
|
-
}
|
|
196
|
-
const transitionCost = neighborValue * crossingPenalty + 1 + turnValue;
|
|
197
262
|
|
|
198
|
-
|
|
199
|
-
|
|
263
|
+
// Cost of traversing to this neighbour
|
|
264
|
+
const transition_cost = neighborValue * crossingPenalty + 1;
|
|
200
265
|
|
|
201
|
-
|
|
266
|
+
// updated path cost
|
|
267
|
+
const gScore = g_score[currentNode] + transition_cost;
|
|
202
268
|
|
|
203
|
-
|
|
204
|
-
came_from[neighbor] = currentNode;
|
|
269
|
+
const index_in_open_set = open.find_index_by_id(neighbor);
|
|
205
270
|
|
|
206
|
-
|
|
271
|
+
const not_in_open_set = index_in_open_set === -1;
|
|
207
272
|
|
|
273
|
+
if (not_in_open_set || gScore < g_score[neighbor]) {
|
|
274
|
+
|
|
275
|
+
// update refined score
|
|
208
276
|
g_score[neighbor] = gScore;
|
|
209
|
-
const h = heuristic(neighbor, goal);
|
|
210
277
|
|
|
211
|
-
|
|
278
|
+
const h = heuristic(neighbor, goal, width);
|
|
212
279
|
|
|
280
|
+
const fScore = gScore + h;
|
|
213
281
|
|
|
214
|
-
if (
|
|
282
|
+
if (not_in_open_set) {
|
|
215
283
|
// Pushing to heap will put it in proper place based on the 'f' value.
|
|
216
|
-
open.
|
|
284
|
+
open.insert(neighbor, fScore);
|
|
217
285
|
} else {
|
|
218
|
-
// Already seen the node, but since it has been
|
|
219
|
-
open.
|
|
286
|
+
// Already seen the node, but since it has been re-scored we need to reorder it in the heap
|
|
287
|
+
open.__update_score_by_index(index_in_open_set, fScore);
|
|
220
288
|
}
|
|
221
289
|
}
|
|
222
290
|
}
|
|
223
291
|
}
|
|
292
|
+
|
|
224
293
|
// No result was found - empty array signifies failure to find path.
|
|
225
294
|
return [];
|
|
226
295
|
}
|
|
@@ -1,32 +1,92 @@
|
|
|
1
|
+
import { randomIntegerBetween } from "../../../core/math/random/randomIntegerBetween.js";
|
|
2
|
+
import { seededRandom } from "../../../core/math/random/seededRandom.js";
|
|
3
|
+
import { number_pretty_print } from "../../../core/primitives/numbers/number_pretty_print.js";
|
|
1
4
|
import { find_path_on_grid_astar } from "./find_path_on_grid_astar.js";
|
|
2
5
|
|
|
3
6
|
test("sanity check on 1x1 grid", () => {
|
|
4
|
-
const path = find_path_on_grid_astar([0], 1, 1, 0, 0, 0,
|
|
7
|
+
const path = find_path_on_grid_astar([0], 1, 1, 0, 0, 0, 1);
|
|
5
8
|
|
|
6
9
|
expect(path).toEqual([0]);
|
|
7
10
|
});
|
|
8
11
|
|
|
9
12
|
test("sanity check on 2x1 grid", () => {
|
|
10
|
-
const path = find_path_on_grid_astar(
|
|
11
|
-
[0, 0], 2, 1,
|
|
12
|
-
0, 1,
|
|
13
|
-
0, 0, 1
|
|
14
|
-
);
|
|
13
|
+
const path = find_path_on_grid_astar([0, 0], 2, 1, 0, 1, 0, 1);
|
|
15
14
|
|
|
16
15
|
expect(path).toEqual([0, 1]);
|
|
17
16
|
});
|
|
18
17
|
|
|
19
18
|
test("sanity check on 2x2 grid", () => {
|
|
20
19
|
|
|
21
|
-
const path = find_path_on_grid_astar(
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
],
|
|
26
|
-
2, 2,
|
|
27
|
-
0, 3,
|
|
28
|
-
0, 0, 1
|
|
29
|
-
);
|
|
20
|
+
const path = find_path_on_grid_astar([
|
|
21
|
+
0, 0,
|
|
22
|
+
1, 0
|
|
23
|
+
], 2, 2, 0, 3, 0, 1);
|
|
30
24
|
|
|
31
25
|
expect(path).toEqual([0, 1, 3]);
|
|
32
26
|
});
|
|
27
|
+
|
|
28
|
+
test("path on open 3x3 grid", () => {
|
|
29
|
+
|
|
30
|
+
const path = find_path_on_grid_astar([
|
|
31
|
+
0, 0, 0,
|
|
32
|
+
0, 0, 0,
|
|
33
|
+
0, 0, 0
|
|
34
|
+
], 3, 3, 0, 8, 0, 1);
|
|
35
|
+
|
|
36
|
+
expect(path).toEqual([0, 6, 8]);
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
test("performance, 256x256 random", () => {
|
|
40
|
+
const FIELD_SIZE = 256;
|
|
41
|
+
|
|
42
|
+
const field = new Uint8Array(FIELD_SIZE * FIELD_SIZE);
|
|
43
|
+
|
|
44
|
+
const random = seededRandom(42);
|
|
45
|
+
|
|
46
|
+
const last_field_index = field.length - 1;
|
|
47
|
+
|
|
48
|
+
for (let i = 0; i < field.length * 0.05; i++) {
|
|
49
|
+
const index = randomIntegerBetween(random, 0, last_field_index);
|
|
50
|
+
|
|
51
|
+
field[index] = 255;
|
|
52
|
+
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
const SAMPLE_COUNT = 10000;
|
|
56
|
+
|
|
57
|
+
const coordinates = new Uint32Array(SAMPLE_COUNT * 2);
|
|
58
|
+
|
|
59
|
+
for (let i = 0; i < SAMPLE_COUNT * 2; i++) {
|
|
60
|
+
//build non-blocking point pairs for rout endpoints
|
|
61
|
+
const index = randomIntegerBetween(random, 0, last_field_index);
|
|
62
|
+
|
|
63
|
+
if (field[index] !== 0) {
|
|
64
|
+
i--;
|
|
65
|
+
continue;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
coordinates[i] = index;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
const t0 = performance.now();
|
|
72
|
+
|
|
73
|
+
for (let i = 0; i < SAMPLE_COUNT; i++) {
|
|
74
|
+
|
|
75
|
+
const i2 = i * 2;
|
|
76
|
+
const start = coordinates[i2];
|
|
77
|
+
const goal = coordinates[i2 + 1];
|
|
78
|
+
|
|
79
|
+
find_path_on_grid_astar(
|
|
80
|
+
field,
|
|
81
|
+
FIELD_SIZE, FIELD_SIZE,
|
|
82
|
+
start,
|
|
83
|
+
goal, 1, 255
|
|
84
|
+
);
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
const t1 = performance.now();
|
|
88
|
+
|
|
89
|
+
const duration = t1 - t0;
|
|
90
|
+
|
|
91
|
+
console.log(`${number_pretty_print(SAMPLE_COUNT)} paths, ${number_pretty_print(duration / SAMPLE_COUNT)}ms per path`);
|
|
92
|
+
});
|