@xtia/grid 0.1.1 → 0.3.0
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/README.md +5 -5
- package/lib/grid.d.ts +29 -2
- package/lib/grid.js +59 -40
- package/package.json +2 -1
package/README.md
CHANGED
|
@@ -43,7 +43,7 @@ const source = imagePipe
|
|
|
43
43
|
.crop(10, 10, 64, 64)
|
|
44
44
|
.scale(.5)
|
|
45
45
|
.rotateLeft();
|
|
46
|
-
const grid = Grid.
|
|
46
|
+
const grid = Grid.init(source);
|
|
47
47
|
```
|
|
48
48
|
|
|
49
49
|
### Wrapping existing structures
|
|
@@ -97,7 +97,7 @@ world.writeMask(mask).fill("mountain");
|
|
|
97
97
|
const lakeRegion = world.region(25, 25, 30, 30);
|
|
98
98
|
|
|
99
99
|
// do we want a distinct, self-contained copy of the region?
|
|
100
|
-
const lakeGrid = Grid.
|
|
100
|
+
const lakeGrid = Grid.init(lakeRegion);
|
|
101
101
|
```
|
|
102
102
|
|
|
103
103
|
## Transformation Layers
|
|
@@ -225,9 +225,9 @@ Unlike visibility maps, which are lazily evaluated according to the supplied `is
|
|
|
225
225
|
|
|
226
226
|
## The storage layer
|
|
227
227
|
|
|
228
|
-
`Grid` itself is an interface
|
|
228
|
+
`Grid` itself is an interface that sits on top of a synchronous 2D read/write mechanism. `GridBase` is a subclass of `Grid` with built-in storage.
|
|
229
229
|
|
|
230
|
-
Although the
|
|
230
|
+
Although the factory methods `Grid.solid<T>()` and `Grid.init<T>()`) belong to `Grid`, they return a `GridBase<T>`. `Grid.wrap<T>()` is an exception, returning `Grid<T>`, as it uses a storage layer provided by the user. The only API distinction is that `GridBase` provides a 'change' event, via `grid.on("change", handler)`.
|
|
231
231
|
|
|
232
232
|
We can perform batched updates, suppressing the 'change' event until a process concludes, with `grid.batchUpdate(callback)`.
|
|
233
233
|
|
|
@@ -238,7 +238,7 @@ Pipe2D makes it easy to save and restore grid data:
|
|
|
238
238
|
```ts
|
|
239
239
|
const snapshot = world.pipe.stash();
|
|
240
240
|
|
|
241
|
-
const restored = Grid.
|
|
241
|
+
const restored = Grid.init(snapshot);
|
|
242
242
|
// or paste it to an existing grid
|
|
243
243
|
world.paste(snapshot);
|
|
244
244
|
```
|
package/lib/grid.d.ts
CHANGED
|
@@ -32,6 +32,31 @@ type CostFunc<T> = (cell: Cell<T>, context: {
|
|
|
32
32
|
traversalType: TraversalType;
|
|
33
33
|
from: Cell<T>;
|
|
34
34
|
}) => number;
|
|
35
|
+
type RectSpec = ({
|
|
36
|
+
left: number;
|
|
37
|
+
width: number;
|
|
38
|
+
right?: undefined;
|
|
39
|
+
} | {
|
|
40
|
+
left: number;
|
|
41
|
+
width?: undefined;
|
|
42
|
+
right: number;
|
|
43
|
+
} | {
|
|
44
|
+
left?: undefined;
|
|
45
|
+
width: number;
|
|
46
|
+
right: number;
|
|
47
|
+
}) & ({
|
|
48
|
+
top: number;
|
|
49
|
+
height: number;
|
|
50
|
+
bottom?: undefined;
|
|
51
|
+
} | {
|
|
52
|
+
top: number;
|
|
53
|
+
height?: undefined;
|
|
54
|
+
bottom: number;
|
|
55
|
+
} | {
|
|
56
|
+
top?: undefined;
|
|
57
|
+
height: number;
|
|
58
|
+
bottom: number;
|
|
59
|
+
});
|
|
35
60
|
export declare class Cell<T> {
|
|
36
61
|
readonly x: number;
|
|
37
62
|
readonly y: number;
|
|
@@ -69,13 +94,15 @@ export declare class Grid<T> {
|
|
|
69
94
|
writeMask(mask: Source2D<boolean> | GetXYFunc<boolean>): Grid<T>;
|
|
70
95
|
[Symbol.iterator](): IterableIterator<T>;
|
|
71
96
|
forEach(callback: (value: T, x: number, y: number) => void): void;
|
|
72
|
-
map<U>(read: (initial: T, x: number, y: number) => U, write: (local: U, x: number, y: number) => T): Grid<U>;
|
|
97
|
+
map<U>(read: (initial: T, x: number, y: number) => U, write: ((local: U, x: number, y: number) => T)): Grid<U>;
|
|
73
98
|
region(x: number, y: number, width: number, height: number): Grid<T>;
|
|
99
|
+
region(rect: RectSpec): Grid<T>;
|
|
74
100
|
scroll(xDelta: number, yDelta: number, fill: T): this;
|
|
75
101
|
static init<T>(width: number, height: number, initCell: (x: number, y: number) => T): GridBase<T>;
|
|
76
|
-
static
|
|
102
|
+
static init<T>(source: Source2D<T>): GridBase<T>;
|
|
77
103
|
static solid<T>(width: number, height: number, fillValue: T): GridBase<T>;
|
|
78
104
|
static wrap<T>(width: number, height: number, get: (x: number, y: number) => T, set: (x: number, y: number, value: T) => void, batch?: (callback: () => void) => void): Grid<T>;
|
|
105
|
+
private assertValidCoordinates;
|
|
79
106
|
}
|
|
80
107
|
export declare class GridBase<T> extends Grid<T> {
|
|
81
108
|
private data;
|
package/lib/grid.js
CHANGED
|
@@ -219,65 +219,52 @@ export class Grid {
|
|
|
219
219
|
throw new RangeError(`Cell coordinates must be bounded integers; got (${x}, ${y})`);
|
|
220
220
|
}
|
|
221
221
|
return new Cell(x, y, this);
|
|
222
|
-
}).
|
|
222
|
+
}).withCache();
|
|
223
223
|
this.pipe = new Pipe2D(this);
|
|
224
|
-
if (!Number.isInteger(width) || !Number.isInteger(height)) {
|
|
225
|
-
throw new Error(`Grid width & height must be
|
|
226
|
-
}
|
|
227
|
-
if (width < 0 || height < 0) {
|
|
228
|
-
throw new Error(`Grid width/height may not be less than 0; got (${width} x ${height})`);
|
|
224
|
+
if (!Number.isInteger(width) || !Number.isInteger(height) || width < 0 || height < 0) {
|
|
225
|
+
throw new Error(`Grid width & height must be non-negative integers; got (${width} x ${height})`);
|
|
229
226
|
}
|
|
230
227
|
}
|
|
231
228
|
batchUpdate(fn) {
|
|
232
229
|
this.batch(fn);
|
|
233
230
|
}
|
|
234
231
|
get(x, y) {
|
|
235
|
-
|
|
236
|
-
throw new Error(`Coordinates must be integer; got (${x}, ${y})`);
|
|
237
|
-
}
|
|
238
|
-
if (x < 0 || x >= this.width || y < 0 || y >= this.height) {
|
|
239
|
-
throw new RangeError("Coordinates out of bounds");
|
|
240
|
-
}
|
|
232
|
+
this.assertValidCoordinates(x, y);
|
|
241
233
|
return this.parentGet(x, y);
|
|
242
234
|
}
|
|
243
235
|
set(x, y, value) {
|
|
244
|
-
|
|
245
|
-
throw new Error(`Coordinates must be integer; got (${x}, ${y})`);
|
|
246
|
-
}
|
|
247
|
-
if (x < 0 || x >= this.width || y < 0 || y >= this.height) {
|
|
248
|
-
throw new RangeError("Coordinates out of bounds");
|
|
249
|
-
}
|
|
236
|
+
this.assertValidCoordinates(x, y);
|
|
250
237
|
this.parentSet(x, y, value);
|
|
251
238
|
}
|
|
252
239
|
trySet(x, y, value) {
|
|
253
240
|
if (x < 0
|
|
254
241
|
|| x >= this.width
|
|
255
242
|
|| y < 0
|
|
256
|
-
|| y >= this.height
|
|
257
|
-
|| !Number.isInteger(x)
|
|
258
|
-
|| !Number.isInteger(y))
|
|
243
|
+
|| y >= this.height)
|
|
259
244
|
return false;
|
|
260
|
-
this.
|
|
245
|
+
this.parentSet(x, y, value);
|
|
261
246
|
return true;
|
|
262
247
|
}
|
|
263
248
|
fill(value) {
|
|
264
249
|
this.batchUpdate(() => {
|
|
265
250
|
for (let y = 0; y < this.height; y++) {
|
|
266
251
|
for (let x = 0; x < this.width; x++) {
|
|
267
|
-
this.
|
|
252
|
+
this.parentSet(x, y, value);
|
|
268
253
|
}
|
|
269
254
|
}
|
|
270
255
|
});
|
|
271
256
|
}
|
|
272
257
|
paste(source, x = 0, y = 0) {
|
|
273
|
-
|
|
258
|
+
this.assertValidCoordinates(x, y, true);
|
|
259
|
+
const sourcePipe = source instanceof Pipe2D ? source : new Pipe2D(source);
|
|
260
|
+
const cachedSource = sourcePipe.stash();
|
|
274
261
|
this.batchUpdate(() => {
|
|
275
262
|
for (let oy = 0; oy < source.height; oy++) {
|
|
276
263
|
for (let ox = 0; ox < source.width; ox++) {
|
|
277
264
|
const tx = ox + x, ty = oy + y;
|
|
278
265
|
if (tx < 0 || tx >= this.width || ty < 0 || ty >= this.height)
|
|
279
266
|
continue;
|
|
280
|
-
this.
|
|
267
|
+
this.parentSet(tx, ty, cachedSource.get(ox, oy));
|
|
281
268
|
}
|
|
282
269
|
}
|
|
283
270
|
});
|
|
@@ -289,7 +276,7 @@ export class Grid {
|
|
|
289
276
|
return new Grid(this.width, this.height, this.parentGet, (x, y, v) => {
|
|
290
277
|
const writable = getMask(x, y);
|
|
291
278
|
if (writable) {
|
|
292
|
-
this.
|
|
279
|
+
this.parentSet(x, y, v);
|
|
293
280
|
}
|
|
294
281
|
}, cb => this.batch(cb));
|
|
295
282
|
}
|
|
@@ -310,12 +297,39 @@ export class Grid {
|
|
|
310
297
|
map(read, write) {
|
|
311
298
|
return new Grid(this.width, this.height, (x, y) => read(this.parentGet(x, y), x, y), (x, y, value) => this.parentSet(x, y, write(value, x, y)), fn => this.batch(fn));
|
|
312
299
|
}
|
|
313
|
-
region(
|
|
314
|
-
|
|
315
|
-
|
|
300
|
+
region(xOrRect, y = 0, width = 0, height = 0) {
|
|
301
|
+
let x;
|
|
302
|
+
if (typeof xOrRect == "number") {
|
|
303
|
+
x = xOrRect;
|
|
316
304
|
}
|
|
317
|
-
|
|
318
|
-
|
|
305
|
+
else {
|
|
306
|
+
if (xOrRect.left === undefined) {
|
|
307
|
+
width = xOrRect.width;
|
|
308
|
+
x = (this.width - 1 - xOrRect.right) - width + 1;
|
|
309
|
+
}
|
|
310
|
+
else {
|
|
311
|
+
x = xOrRect.left;
|
|
312
|
+
width = xOrRect.width === undefined
|
|
313
|
+
? (this.width - 1 - xOrRect.right) - x + 1
|
|
314
|
+
: xOrRect.width;
|
|
315
|
+
}
|
|
316
|
+
if (xOrRect.top === undefined) {
|
|
317
|
+
height = xOrRect.height;
|
|
318
|
+
y = (this.height - 1 - xOrRect.bottom) - height + 1;
|
|
319
|
+
}
|
|
320
|
+
else {
|
|
321
|
+
y = xOrRect.top;
|
|
322
|
+
height = xOrRect.height === undefined
|
|
323
|
+
? (this.height - 1 - xOrRect.bottom) - y + 1
|
|
324
|
+
: xOrRect.height;
|
|
325
|
+
}
|
|
326
|
+
}
|
|
327
|
+
this.assertValidCoordinates(x, y);
|
|
328
|
+
if (!Number.isInteger(width) || !Number.isInteger(height)) {
|
|
329
|
+
throw new Error("Region dimensions must be integer");
|
|
330
|
+
}
|
|
331
|
+
if (x + width > this.width || y + height > this.height) {
|
|
332
|
+
throw new RangeError("Requested region exceeds the parent's bounds");
|
|
319
333
|
}
|
|
320
334
|
return new Grid(width, height, (tx, ty) => {
|
|
321
335
|
return this.parentGet(tx + x, ty + y);
|
|
@@ -324,9 +338,7 @@ export class Grid {
|
|
|
324
338
|
}, fn => this.batch(fn));
|
|
325
339
|
}
|
|
326
340
|
scroll(xDelta, yDelta, fill) {
|
|
327
|
-
|
|
328
|
-
throw new Error(`Scroll offsets must be integer; got (${xDelta}, ${yDelta})`);
|
|
329
|
-
}
|
|
341
|
+
this.assertValidCoordinates(xDelta, yDelta, true);
|
|
330
342
|
this.paste(this, xDelta, yDelta);
|
|
331
343
|
if (xDelta > 0) {
|
|
332
344
|
this.region(0, 0, xDelta, this.height).fill(fill);
|
|
@@ -342,22 +354,29 @@ export class Grid {
|
|
|
342
354
|
}
|
|
343
355
|
return this;
|
|
344
356
|
}
|
|
345
|
-
static init(
|
|
357
|
+
static init(widthOrSource, height, initCell) {
|
|
358
|
+
if (typeof widthOrSource == "object")
|
|
359
|
+
return new GridBase(widthOrSource);
|
|
346
360
|
return new GridBase({
|
|
347
|
-
width,
|
|
348
|
-
height,
|
|
361
|
+
width: widthOrSource,
|
|
362
|
+
height: height,
|
|
349
363
|
get: initCell
|
|
350
364
|
});
|
|
351
365
|
}
|
|
352
|
-
static from(source) {
|
|
353
|
-
return new GridBase(source);
|
|
354
|
-
}
|
|
355
366
|
static solid(width, height, fillValue) {
|
|
356
367
|
return new GridBase(Pipe2D.solid(fillValue, width, height));
|
|
357
368
|
}
|
|
358
369
|
static wrap(width, height, get, set, batch = cb => cb()) {
|
|
359
370
|
return new Grid(width, height, get, set, batch);
|
|
360
371
|
}
|
|
372
|
+
assertValidCoordinates(x, y, ignoreBounds = false) {
|
|
373
|
+
if (!Number.isInteger(x) || !Number.isInteger(y)) {
|
|
374
|
+
throw new Error(`Coordinates must be integer; got (${x}, ${y})`);
|
|
375
|
+
}
|
|
376
|
+
if (!ignoreBounds && (x < 0 || x >= this.width || y < 0 || y >= this.height)) {
|
|
377
|
+
throw new RangeError(`Coordinates out of bounds (${x}, ${y}) / (${this.width}, ${this.height})`);
|
|
378
|
+
}
|
|
379
|
+
}
|
|
361
380
|
}
|
|
362
381
|
export class GridBase extends Grid {
|
|
363
382
|
triggerEvent(name, data) {
|
package/package.json
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
{
|
|
2
|
-
"version": "0.
|
|
2
|
+
"version": "0.3.0",
|
|
3
3
|
"description": "2D store and utilities, with zero-copy regions and transform layers, pathfinding, Pipe2D interop",
|
|
4
4
|
"name": "@xtia/grid",
|
|
5
5
|
"exports": {
|
|
@@ -21,6 +21,7 @@
|
|
|
21
21
|
"files": [
|
|
22
22
|
"lib/"
|
|
23
23
|
],
|
|
24
|
+
"license": "MIT",
|
|
24
25
|
"devDependencies": {
|
|
25
26
|
"@vitest/coverage-v8": "^4.1.10",
|
|
26
27
|
"typescript": "^7.0.2",
|