@xtia/grid 0.0.5 → 0.0.7
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 +35 -7
- package/lib/grid.d.ts +8 -6
- package/lib/grid.js +31 -18
- package/package.json +2 -1
package/README.md
CHANGED
|
@@ -11,8 +11,8 @@ A mutable 2D grid with efficient region views, spatial operations, and seamless
|
|
|
11
11
|
- **Mutable 2D storage** with type safety
|
|
12
12
|
- **Zero-copy region views** - edit or provide subgrids and transformation layers without copying
|
|
13
13
|
- **Spatial operations** - fill, paste, flood fill, pathfinding
|
|
14
|
-
- **Pipe2D
|
|
15
|
-
- **Consistent API** - for grids and
|
|
14
|
+
- **First-class Pipe2D interoperability** – use grids as sources for pipes, or pipes as sources for grids
|
|
15
|
+
- **Consistent API** - for grids, regions, and your existing 2D structures
|
|
16
16
|
- **Mask support** - apply operations to arbitrary shapes
|
|
17
17
|
|
|
18
18
|
## Example
|
|
@@ -22,7 +22,7 @@ npm i @xtia/grid
|
|
|
22
22
|
```
|
|
23
23
|
|
|
24
24
|
```ts
|
|
25
|
-
import { Grid } from "@xtia/grid"; // ~
|
|
25
|
+
import { Grid } from "@xtia/grid"; // ~4.8kb gzipped
|
|
26
26
|
|
|
27
27
|
// initialise a 30x20 Grid<number> of 0's
|
|
28
28
|
const numGrid = Grid.solid(30, 20, 0);
|
|
@@ -46,6 +46,34 @@ const source = imagePipe
|
|
|
46
46
|
const grid = Grid.from(source);
|
|
47
47
|
```
|
|
48
48
|
|
|
49
|
+
### Wrapping existing structures
|
|
50
|
+
|
|
51
|
+
Use Grid's interface over any read/write 2D structure:
|
|
52
|
+
|
|
53
|
+
```ts
|
|
54
|
+
const gameMap = [
|
|
55
|
+
[0, 1, -1, 2],
|
|
56
|
+
[1, 0, 1, 0],
|
|
57
|
+
[-1, 1, 0, 2],
|
|
58
|
+
[2, 0, 2, -1]
|
|
59
|
+
];
|
|
60
|
+
|
|
61
|
+
// create a *live view* Grid over gameMap
|
|
62
|
+
const grid = Grid.wrap(
|
|
63
|
+
gameMap[0].length, // width
|
|
64
|
+
gameMap.length, // height
|
|
65
|
+
(x, y) => gameMap[y][x], // get
|
|
66
|
+
(x, y, value) => gameMap[y][x] = value // set
|
|
67
|
+
);
|
|
68
|
+
|
|
69
|
+
// reading the grid = reading the source
|
|
70
|
+
gameMap[0][0] = 50;
|
|
71
|
+
console.log(grid.get(0, 0)); // 50
|
|
72
|
+
// writing to the grid = writing to the source
|
|
73
|
+
grid.set(3, 3, 100);
|
|
74
|
+
console.log(gameMap[3][3]); // 100
|
|
75
|
+
```
|
|
76
|
+
|
|
49
77
|
## Regions
|
|
50
78
|
|
|
51
79
|
Use `grid.region(x, y, w, h)` to define a subgrid. The subgrid is **zero-copy view** into the parent; changes to the subgrid affect the parent and vice-versa.
|
|
@@ -106,7 +134,7 @@ const mountainCells = world.cells.toFlatArrayXY()
|
|
|
106
134
|
|
|
107
135
|
## Cell interface
|
|
108
136
|
|
|
109
|
-
`grid.cells` provides a Pipe2D, for convenient transformation, of
|
|
137
|
+
`grid.cells` provides a Pipe2D, for convenient transformation, of Cell objects, each representing a live view into a grid location.
|
|
110
138
|
|
|
111
139
|
```ts
|
|
112
140
|
const topLeft = world.cells.get(0, 0);
|
|
@@ -176,15 +204,15 @@ const pathToCentre = pathMap.get(50, 50);
|
|
|
176
204
|
const pathToCorner = pathMap.get(99, 99);
|
|
177
205
|
```
|
|
178
206
|
|
|
179
|
-
Unlike visibility maps, which are lazily evaluated according to the supplied `
|
|
207
|
+
Unlike visibility maps, which are lazily evaluated according to the supplied `isClear` function *when the map is queried* (but can be easily cached with `visMap.withCache()`), path maps use a relatively expensive traversal map that's created *when the map is created*. This distinction is hinted through the `create*` vs `get*` naming.
|
|
180
208
|
|
|
181
209
|
## The storage layer
|
|
182
210
|
|
|
183
211
|
`Grid` itself is an interface for reading and writing 2D data. `GridBase` - a subclass of `Grid` - maintains the actual storage of such data.
|
|
184
212
|
|
|
185
|
-
Although the Grid factory methods (`Grid.solid()`, `Grid.from()`, `Grid.init()`) return a `GridBase<T
|
|
213
|
+
Although the Grid factory methods (`Grid.solid<T>()`, `Grid.from<T>()`, `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)`.
|
|
186
214
|
|
|
187
|
-
|
|
215
|
+
We can perform batched updates, suppressing the 'change' event until a process concludes, with `grid.batchUpdate(callback)`.
|
|
188
216
|
|
|
189
217
|
## Save and load
|
|
190
218
|
|
package/lib/grid.d.ts
CHANGED
|
@@ -16,12 +16,13 @@ type PathFindOptions<T> = {
|
|
|
16
16
|
shortcutMap?: Source2D<Cell<T>[] | undefined>;
|
|
17
17
|
};
|
|
18
18
|
type CostMapOptions<T> = PathFindOptions<T> & {
|
|
19
|
-
stopAt?:
|
|
19
|
+
stopAt?: LocationSpec<T>;
|
|
20
20
|
};
|
|
21
21
|
type VisibilityOptions = {
|
|
22
22
|
maxDistance?: number;
|
|
23
23
|
boundariesVisible?: boolean;
|
|
24
24
|
};
|
|
25
|
+
type LocationSpec<T> = Cell<T> | [number, number];
|
|
25
26
|
export declare enum TraversalType {
|
|
26
27
|
cardinal = 0,
|
|
27
28
|
diagonal = 1,
|
|
@@ -38,16 +39,16 @@ export declare class Cell<T> {
|
|
|
38
39
|
constructor(x: number, y: number, gridView: Grid<T>);
|
|
39
40
|
get value(): T;
|
|
40
41
|
set value(v: T);
|
|
42
|
+
private getSiblingCell;
|
|
43
|
+
private getSiblingXY;
|
|
41
44
|
getNeighbours(includeDiagonals?: boolean): Cell<T>[];
|
|
42
45
|
look(xDelta: number, yDelta: number): Cell<T> | undefined;
|
|
43
|
-
findPath(target:
|
|
46
|
+
findPath(target: LocationSpec<T>, getCost: CostFunc<T>, options?: PathFindOptions<T>): Cell<T>[] | null;
|
|
44
47
|
getPathMap(getCost: CostFunc<T>, options?: PathFindOptions<T>): Pipe2D<Cell<T>[] | null>;
|
|
45
48
|
getCostMap(getCost: CostFunc<T>, options?: CostMapOptions<T>): Pipe2D<number>;
|
|
46
49
|
private calculateCosts;
|
|
47
|
-
getLineTo(target:
|
|
48
|
-
|
|
49
|
-
y: number;
|
|
50
|
-
} | [number, number]): IterableIterator<Cell<T>>;
|
|
50
|
+
getLineTo(target: LocationSpec<T>): IterableIterator<Cell<T>>;
|
|
51
|
+
getLineTo(x: number, y: number): IterableIterator<Cell<T>>;
|
|
51
52
|
createVisibilityMap(isClear: (cell: Cell<T>) => boolean, options?: VisibilityOptions): Pipe2D<boolean>;
|
|
52
53
|
}
|
|
53
54
|
export declare class Grid<T> {
|
|
@@ -77,6 +78,7 @@ export declare class Grid<T> {
|
|
|
77
78
|
static init<T>(width: number, height: number, initCell: (x: number, y: number) => T): GridBase<T>;
|
|
78
79
|
static from<T>(source: Source2D<T>): GridBase<T>;
|
|
79
80
|
static solid<T>(width: number, height: number, fillValue: T): GridBase<T>;
|
|
81
|
+
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>;
|
|
80
82
|
}
|
|
81
83
|
export declare class GridBase<T> extends Grid<T> {
|
|
82
84
|
private data;
|
package/lib/grid.js
CHANGED
|
@@ -18,6 +18,22 @@ export class Cell {
|
|
|
18
18
|
set value(v) {
|
|
19
19
|
this.gridView.set(this.x, this.y, v);
|
|
20
20
|
}
|
|
21
|
+
getSiblingCell(location) {
|
|
22
|
+
if (Array.isArray(location))
|
|
23
|
+
return this.gridView.cells.get(...location);
|
|
24
|
+
if (location.gridView !== this.gridView) {
|
|
25
|
+
throw new Error("Target cell belongs to a different Grid");
|
|
26
|
+
}
|
|
27
|
+
return location;
|
|
28
|
+
}
|
|
29
|
+
getSiblingXY(location) {
|
|
30
|
+
if (Array.isArray(location))
|
|
31
|
+
return location;
|
|
32
|
+
if (location.gridView !== this.gridView) {
|
|
33
|
+
throw new Error("Target cell belongs to a different Grid");
|
|
34
|
+
}
|
|
35
|
+
return [location.x, location.y];
|
|
36
|
+
}
|
|
21
37
|
getNeighbours(includeDiagonals = false) {
|
|
22
38
|
const offsets = includeDiagonals
|
|
23
39
|
? [[-1, -1], [-1, 0], [-1, 1], [0, -1], [0, 1], [1, -1], [1, 0], [1, 1]]
|
|
@@ -34,15 +50,12 @@ export class Cell {
|
|
|
34
50
|
return this.gridView.cells.get(xDelta + this.x, yDelta + this.y);
|
|
35
51
|
}
|
|
36
52
|
findPath(target, getCost, options = {}) {
|
|
37
|
-
const targetXY = Array.isArray(target)
|
|
38
|
-
? target
|
|
39
|
-
: [target.x, target.y];
|
|
40
53
|
const pathMap = this.calculateCosts(getCost, {
|
|
41
|
-
stopAt:
|
|
54
|
+
stopAt: target,
|
|
42
55
|
allowDiagonal: options.allowDiagonal,
|
|
43
56
|
maxCost: options.maxCost,
|
|
44
57
|
}, true);
|
|
45
|
-
return pathMap === null || pathMap === void 0 ? void 0 : pathMap.get(...
|
|
58
|
+
return pathMap === null || pathMap === void 0 ? void 0 : pathMap.get(...this.getSiblingXY(target));
|
|
46
59
|
}
|
|
47
60
|
getPathMap(getCost, options = {}) {
|
|
48
61
|
return this.calculateCosts(getCost, options, true);
|
|
@@ -61,24 +74,21 @@ export class Cell {
|
|
|
61
74
|
: null;
|
|
62
75
|
costs.set(this, 0);
|
|
63
76
|
const queue = new OrderedQueue(cell => { var _a; return (_a = costs.get(cell)) !== null && _a !== void 0 ? _a : Infinity; }, this);
|
|
64
|
-
const
|
|
65
|
-
? this.gridView.cells.get(options.stopAt[0], options.stopAt[1])
|
|
66
|
-
: options.stopAt;
|
|
67
|
-
if (targetCell && targetCell.gridView !== this.gridView) {
|
|
68
|
-
throw new Error("Target cell belongs to a different Grid");
|
|
69
|
-
}
|
|
77
|
+
const stopAtCell = options.stopAt && this.getSiblingCell(options.stopAt);
|
|
70
78
|
while (queue.length > 0) {
|
|
71
79
|
const current = queue.take();
|
|
72
80
|
(_a = options.onVisit) === null || _a === void 0 ? void 0 : _a.call(options, { cell: current, cost: costs.get(current) });
|
|
73
81
|
if (visited.has(current))
|
|
74
82
|
continue;
|
|
75
83
|
visited.add(current);
|
|
76
|
-
if (
|
|
84
|
+
if (stopAtCell && current === stopAtCell) {
|
|
77
85
|
break;
|
|
78
86
|
}
|
|
79
87
|
const currentCost = costs.get(current);
|
|
80
88
|
const neighbours = current.getNeighbours(options.allowDiagonal).map(cell => ({
|
|
81
|
-
traversalType: cell.x == current.x && cell.y == current.y
|
|
89
|
+
traversalType: cell.x == current.x && cell.y == current.y
|
|
90
|
+
? TraversalType.cardinal
|
|
91
|
+
: TraversalType.diagonal,
|
|
82
92
|
cell
|
|
83
93
|
}));
|
|
84
94
|
if (options.shortcutMap) {
|
|
@@ -135,10 +145,10 @@ export class Cell {
|
|
|
135
145
|
return this.gridView.cells.map(costs, () => Infinity)
|
|
136
146
|
.strict();
|
|
137
147
|
}
|
|
138
|
-
*getLineTo(
|
|
139
|
-
const [targetX, targetY] =
|
|
140
|
-
?
|
|
141
|
-
:
|
|
148
|
+
*getLineTo(targetOrX, _targetY) {
|
|
149
|
+
const [targetX, targetY] = typeof targetOrX == "number"
|
|
150
|
+
? [targetOrX, _targetY]
|
|
151
|
+
: this.getSiblingXY(targetOrX);
|
|
142
152
|
if (this.x === targetX && this.y === targetY) {
|
|
143
153
|
yield this;
|
|
144
154
|
return;
|
|
@@ -182,7 +192,7 @@ export class Cell {
|
|
|
182
192
|
return false;
|
|
183
193
|
}
|
|
184
194
|
}
|
|
185
|
-
for (const cell of this.getLineTo(
|
|
195
|
+
for (const cell of this.getLineTo([x, y])) {
|
|
186
196
|
if (isClear(cell))
|
|
187
197
|
continue;
|
|
188
198
|
return cell.x === x && cell.y === y
|
|
@@ -314,6 +324,9 @@ export class Grid {
|
|
|
314
324
|
static solid(width, height, fillValue) {
|
|
315
325
|
return new GridBase(Pipe2D.solid(fillValue, width, height));
|
|
316
326
|
}
|
|
327
|
+
static wrap(width, height, get, set, batch = cb => cb()) {
|
|
328
|
+
return new Grid(width, height, get, set, batch);
|
|
329
|
+
}
|
|
317
330
|
}
|
|
318
331
|
export class GridBase extends Grid {
|
|
319
332
|
triggerEvent(name, data) {
|
package/package.json
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
{
|
|
2
|
-
"version": "0.0.
|
|
2
|
+
"version": "0.0.7",
|
|
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": {
|
|
@@ -9,6 +9,7 @@
|
|
|
9
9
|
},
|
|
10
10
|
"./lib/*": null
|
|
11
11
|
},
|
|
12
|
+
"keywords": ["grid", "game-dev", "pathfinding", "cellular-automata"],
|
|
12
13
|
"scripts": {
|
|
13
14
|
"build": "npx tsc",
|
|
14
15
|
"test": "npx vitest run"
|