@xtia/grid 0.0.2 → 0.0.4
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 +176 -0
- package/lib/grid.d.ts +7 -8
- package/lib/grid.js +29 -24
- package/lib/index.d.ts +1 -1
- package/lib/index.js +1 -1
- package/package.json +2 -2
package/README.md
ADDED
|
@@ -0,0 +1,176 @@
|
|
|
1
|
+
# `Grid`
|
|
2
|
+
|
|
3
|
+
**Alpha**: Some API details may change
|
|
4
|
+
|
|
5
|
+
## Summary
|
|
6
|
+
|
|
7
|
+
A mutable 2D grid with efficient region views, spatial operations, and seamless interoperability with [Pipe2D](https://github.com/tiadrop/pipe2d). Store, edit, and transform 2D data with an ergonomic API designed for practical use.
|
|
8
|
+
|
|
9
|
+
## Features
|
|
10
|
+
|
|
11
|
+
- **Mutable 2D storage** with type safety
|
|
12
|
+
- **Zero-copy region views** - edit or provide subgrids and transformation layers without copying
|
|
13
|
+
- **Spatial operations** - fill, paste, flood fill, pathfinding
|
|
14
|
+
- **Pipe2D integration** - materialise pipes, get pipe views
|
|
15
|
+
- **Consistent API** - for grids and regions
|
|
16
|
+
- **Mask support** - apply operations to arbitrary shapes
|
|
17
|
+
|
|
18
|
+
## Example
|
|
19
|
+
|
|
20
|
+
```
|
|
21
|
+
npm i @xtia/grid
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
```ts
|
|
25
|
+
import { Grid } from "@xtia/grid";
|
|
26
|
+
|
|
27
|
+
// initialise a 30x20 Grid<number> of 0's
|
|
28
|
+
const numGrid = Grid.solid(30, 20, 0);
|
|
29
|
+
|
|
30
|
+
// change a value
|
|
31
|
+
numGrid.set(3, 3, 50);
|
|
32
|
+
|
|
33
|
+
// initialise a chessboard
|
|
34
|
+
const chessGrid = Grid.init(8, 8, (x, y) =>
|
|
35
|
+
(x + y) % 2 === 0 ? 'black' : 'white'
|
|
36
|
+
);
|
|
37
|
+
|
|
38
|
+
// read a value
|
|
39
|
+
const colour = chessGrid.get(4, 5);
|
|
40
|
+
|
|
41
|
+
// initialise a grid from a Pipe2D
|
|
42
|
+
const source = imagePipe
|
|
43
|
+
.crop(10, 10, 64, 64)
|
|
44
|
+
.scale(.5)
|
|
45
|
+
.rotateLeft();
|
|
46
|
+
const grid = Grid.from(source);
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
## Regions
|
|
50
|
+
|
|
51
|
+
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.
|
|
52
|
+
|
|
53
|
+
```ts
|
|
54
|
+
// game map with terrain
|
|
55
|
+
const world = Grid.solid(100, 100, "grass"); // Grid<string>
|
|
56
|
+
|
|
57
|
+
// add a lake
|
|
58
|
+
world.region(30, 30, 20, 20).fill("water");
|
|
59
|
+
|
|
60
|
+
// add mountains around the lake with a circular mask
|
|
61
|
+
const centre = world.cells.get(40, 40);
|
|
62
|
+
const mask = (x: number, y: number) => {
|
|
63
|
+
const dist = Math.hypot(x - centre.x, y - centre.y);
|
|
64
|
+
return dist > 12 && dist < 18; // mountain ring
|
|
65
|
+
};
|
|
66
|
+
world.fill("mountain", mask);
|
|
67
|
+
|
|
68
|
+
// get a view of just the interesting area
|
|
69
|
+
const lakeRegion = world.region(25, 25, 30, 30);
|
|
70
|
+
|
|
71
|
+
// do we want a distinct, self-contained copy of the region?
|
|
72
|
+
const lakeGrid = Grid.from(lakeRegion);
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
## Transformation Layers
|
|
76
|
+
|
|
77
|
+
`grid.map(read, write)` creates a **view** that reads and writes the parent through the provided transformation functions.
|
|
78
|
+
|
|
79
|
+
```ts
|
|
80
|
+
const spriteMap = world.map(
|
|
81
|
+
type => type + ".png",
|
|
82
|
+
sprite => sprite.replace(/\.*/, '')
|
|
83
|
+
);
|
|
84
|
+
|
|
85
|
+
world.set(0, 0, "mountain"); // modify the parent
|
|
86
|
+
console.log(spriteMap.get(0, 0)); // read the transformed view: "mountain.png"
|
|
87
|
+
spriteMap.set(1, 0, "forest.png"); // modify the view
|
|
88
|
+
console.log(world.get(1, 0)); // read the parent: "forest"
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
For one-way read-mapping, use `grid.pipe` - a [Pipe2D](https://github.com/tiadrop/pipe2d) view into the grid's data.
|
|
92
|
+
|
|
93
|
+
## Cell interface
|
|
94
|
+
|
|
95
|
+
`grid.cells` provides a Pipe2D, for convenient transformation, of live-view interfaces relating to positions in the grid.
|
|
96
|
+
|
|
97
|
+
```ts
|
|
98
|
+
const topLeft = world.cells.get(0, 0);
|
|
99
|
+
console.log(topLeft.value); // "mountain"
|
|
100
|
+
|
|
101
|
+
topLeft.value = "forest; // modifies the underlying data
|
|
102
|
+
console.log(spriteMap.get(0, 0)); // now "forest.png";
|
|
103
|
+
|
|
104
|
+
// navigate by offset
|
|
105
|
+
const adjacentCell = topLeft.look(1, 0);
|
|
106
|
+
console.log(adjacentCell.x, adjacentCell.y); // 1, 0
|
|
107
|
+
```
|
|
108
|
+
|
|
109
|
+
Cells provide methods for locational utilities such as pathfinding and visibility mapping.
|
|
110
|
+
|
|
111
|
+
Cells are unique to, owned by, and coordinated relative to the view that provided them. Their pathfinding and visibility mapping features are unaware of space outside of their view's bounds.
|
|
112
|
+
|
|
113
|
+
### (Everybody needs good) Neighbours
|
|
114
|
+
|
|
115
|
+
`cell.getNeighbours(includeDiagonals?)` returns an array of Cells:
|
|
116
|
+
|
|
117
|
+
```ts
|
|
118
|
+
// derive a display number for clicked cells in Minesweeper
|
|
119
|
+
const numOfAdjacentMines = clickedCell.getNeighbours(true)
|
|
120
|
+
.filter(cell => cell.value.isMine)
|
|
121
|
+
.length;
|
|
122
|
+
```
|
|
123
|
+
|
|
124
|
+
### Path finding
|
|
125
|
+
```ts
|
|
126
|
+
const costs = {
|
|
127
|
+
grass: 1,
|
|
128
|
+
water: Infinity,
|
|
129
|
+
mountain: Infinity,
|
|
130
|
+
forest: 2
|
|
131
|
+
};
|
|
132
|
+
|
|
133
|
+
const start = world.cells.get(5, 5);
|
|
134
|
+
const destination = world.cells.get(90, 90);
|
|
135
|
+
const path = start.findPath(
|
|
136
|
+
destination, // or simply [90, 90]
|
|
137
|
+
(cell) => costs[cell.value]
|
|
138
|
+
); // Cell<string>[]
|
|
139
|
+
```
|
|
140
|
+
|
|
141
|
+
### Visibility mapping
|
|
142
|
+
|
|
143
|
+
```ts
|
|
144
|
+
// createVisibilityMap(isClear);
|
|
145
|
+
const visibility = start.createVisibilityMap(
|
|
146
|
+
cell => cell.value === "grass" // anything except grass blocks vision
|
|
147
|
+
);
|
|
148
|
+
|
|
149
|
+
// any 2d source, including visibility maps, can be used as a mask for paste/fill operations
|
|
150
|
+
// paste(source: Source2D<T>, mask?: Source2D<boolean>)
|
|
151
|
+
screenGrid.paste(spriteMap, visibility);
|
|
152
|
+
|
|
153
|
+
// using pipe2d's convenience
|
|
154
|
+
const mountainCells = world.cells.toFlatArrayXY().filter(c => c.value === "mountain");
|
|
155
|
+
```
|
|
156
|
+
|
|
157
|
+
### Reusing path data
|
|
158
|
+
|
|
159
|
+
Use `cell.getPathMap(costFunc)` to create a Pipe2D of optimal paths from the parent cell. The grid space is explored once to produce an internal traversal map, and paths to individual cells are constructed from that data (and cached) when that pipe is queried.
|
|
160
|
+
|
|
161
|
+
```ts
|
|
162
|
+
const pathMap = start.getPathMap(c => costs[c.value]);
|
|
163
|
+
|
|
164
|
+
const pathToCentre = pathMap.get(50, 50);
|
|
165
|
+
const pathToCorner = pathMap.get(99, 99);
|
|
166
|
+
```
|
|
167
|
+
|
|
168
|
+
Unlike visibility maps, which are lazily evaluated according to the supplied `isWall` function *when the map is queried*, path maps necessarily reflect the underlying data *when the map is created*. This distinction is hinted through the `create*` vs `get*` naming.
|
|
169
|
+
|
|
170
|
+
## The storage layer
|
|
171
|
+
|
|
172
|
+
`Grid` itself is an interface for reading and writing 2D data. `GridBase` - a subclass of `Grid` - maintains the actual storage of such data.
|
|
173
|
+
|
|
174
|
+
Although the Grid factory methods (`Grid.solid()`, `Grid.from()`, `Grid.init()`) return a `GridBase<T>`, the mental model is that we're simply working with `Grid`s, therefore those factory methods live on `Grid`. The only API distinction is that `GridBase` provides a 'change' event, via `grid.on("change", handler)`.
|
|
175
|
+
|
|
176
|
+
You can perform batched updates, holding off the 'change' event until the batch process concludes, with `grid.batchUpdate(callback)`.
|
package/lib/grid.d.ts
CHANGED
|
@@ -18,6 +18,10 @@ type PathFindOptions<T> = {
|
|
|
18
18
|
type CostMapOptions<T> = PathFindOptions<T> & {
|
|
19
19
|
stopAt?: [number, number] | Cell<T>;
|
|
20
20
|
};
|
|
21
|
+
type VisibilityOptions = {
|
|
22
|
+
maxDistance?: number;
|
|
23
|
+
boundariesVisible?: boolean;
|
|
24
|
+
};
|
|
21
25
|
export declare enum TraversalType {
|
|
22
26
|
cardinal = 0,
|
|
23
27
|
diagonal = 1,
|
|
@@ -27,11 +31,6 @@ type CostFunc<T> = (cell: Cell<T>, context: {
|
|
|
27
31
|
traversalType: TraversalType;
|
|
28
32
|
from: Cell<T>;
|
|
29
33
|
}) => number;
|
|
30
|
-
export declare enum Visibility {
|
|
31
|
-
visible = 0,
|
|
32
|
-
hidden = 1,
|
|
33
|
-
wall = 2
|
|
34
|
-
}
|
|
35
34
|
export declare class Cell<T> {
|
|
36
35
|
readonly x: number;
|
|
37
36
|
readonly y: number;
|
|
@@ -49,7 +48,7 @@ export declare class Cell<T> {
|
|
|
49
48
|
x: number;
|
|
50
49
|
y: number;
|
|
51
50
|
} | [number, number]): IterableIterator<Cell<T>>;
|
|
52
|
-
createVisibilityMap(
|
|
51
|
+
createVisibilityMap(isClear: (cell: Cell<T>) => boolean, options?: VisibilityOptions): Pipe2D<boolean>;
|
|
53
52
|
}
|
|
54
53
|
export declare class Grid<T> {
|
|
55
54
|
readonly width: number;
|
|
@@ -64,8 +63,8 @@ export declare class Grid<T> {
|
|
|
64
63
|
get(x: number, y: number): T;
|
|
65
64
|
set(x: number, y: number, value: T): void;
|
|
66
65
|
trySet(x: number, y: number, value: T): boolean;
|
|
67
|
-
fill(value: T, mask?: Source2D<boolean>): void;
|
|
68
|
-
paste(x: number, y: number, source: Source2D<T>, mask?: Source2D<boolean>): void;
|
|
66
|
+
fill(value: T, mask?: Source2D<boolean> | GetXYFunc<boolean>): void;
|
|
67
|
+
paste(x: number, y: number, source: Source2D<T>, mask?: Source2D<boolean> | GetXYFunc<boolean>): void;
|
|
69
68
|
[Symbol.iterator](): IterableIterator<{
|
|
70
69
|
x: number;
|
|
71
70
|
y: number;
|
package/lib/grid.js
CHANGED
|
@@ -6,12 +6,6 @@ export var TraversalType;
|
|
|
6
6
|
TraversalType[TraversalType["diagonal"] = 1] = "diagonal";
|
|
7
7
|
TraversalType[TraversalType["shortcut"] = 2] = "shortcut";
|
|
8
8
|
})(TraversalType || (TraversalType = {}));
|
|
9
|
-
export var Visibility;
|
|
10
|
-
(function (Visibility) {
|
|
11
|
-
Visibility[Visibility["visible"] = 0] = "visible";
|
|
12
|
-
Visibility[Visibility["hidden"] = 1] = "hidden";
|
|
13
|
-
Visibility[Visibility["wall"] = 2] = "wall";
|
|
14
|
-
})(Visibility || (Visibility = {}));
|
|
15
9
|
export class Cell {
|
|
16
10
|
constructor(x, y, gridView) {
|
|
17
11
|
this.x = x;
|
|
@@ -175,26 +169,27 @@ export class Cell {
|
|
|
175
169
|
}
|
|
176
170
|
}
|
|
177
171
|
}
|
|
178
|
-
createVisibilityMap(
|
|
172
|
+
createVisibilityMap(isClear, options = {}) {
|
|
173
|
+
var _a, _b;
|
|
174
|
+
const maxDistance = (_a = options.maxDistance) !== null && _a !== void 0 ? _a : Infinity;
|
|
175
|
+
const boundariesVisible = (_b = options.boundariesVisible) !== null && _b !== void 0 ? _b : true;
|
|
179
176
|
return new Pipe2D(this.gridView.width, this.gridView.height, (x, y) => {
|
|
180
|
-
if (
|
|
181
|
-
return
|
|
182
|
-
const distance = getDistance(this, { x, y });
|
|
183
|
-
if (distance > maxDistance) {
|
|
184
|
-
return Visibility.hidden;
|
|
177
|
+
if (!isClear(this)) {
|
|
178
|
+
return x === this.x && y === this.y ? boundariesVisible : false;
|
|
185
179
|
}
|
|
186
|
-
|
|
187
|
-
if (
|
|
188
|
-
|
|
189
|
-
continue;
|
|
190
|
-
}
|
|
191
|
-
if (cell.x === x && cell.y === y) {
|
|
192
|
-
return Visibility.wall;
|
|
193
|
-
}
|
|
194
|
-
return Visibility.hidden;
|
|
180
|
+
if (maxDistance < Infinity) {
|
|
181
|
+
if (getDistance(this, { x, y }) > maxDistance) {
|
|
182
|
+
return false;
|
|
195
183
|
}
|
|
196
184
|
}
|
|
197
|
-
|
|
185
|
+
for (const cell of this.getLineTo({ x, y })) {
|
|
186
|
+
if (isClear(cell))
|
|
187
|
+
continue;
|
|
188
|
+
return cell.x === x && cell.y === y
|
|
189
|
+
? boundariesVisible
|
|
190
|
+
: false;
|
|
191
|
+
}
|
|
192
|
+
return true;
|
|
198
193
|
});
|
|
199
194
|
}
|
|
200
195
|
}
|
|
@@ -235,10 +230,15 @@ export class Grid {
|
|
|
235
230
|
return true;
|
|
236
231
|
}
|
|
237
232
|
fill(value, mask) {
|
|
233
|
+
const maskSource = typeof mask == "function" ? {
|
|
234
|
+
width: this.width,
|
|
235
|
+
height: this.height,
|
|
236
|
+
get: mask
|
|
237
|
+
} : mask;
|
|
238
238
|
this.batchUpdate(() => {
|
|
239
239
|
for (let y = 0; y < this.height; y++) {
|
|
240
240
|
for (let x = 0; x < this.width; x++) {
|
|
241
|
-
if (
|
|
241
|
+
if (maskSource && !maskSource.get(x, y))
|
|
242
242
|
continue;
|
|
243
243
|
this.trySet(x, y, value);
|
|
244
244
|
}
|
|
@@ -246,13 +246,18 @@ export class Grid {
|
|
|
246
246
|
});
|
|
247
247
|
}
|
|
248
248
|
paste(x, y, source, mask) {
|
|
249
|
+
const maskSource = typeof mask == "function" ? {
|
|
250
|
+
width: this.width,
|
|
251
|
+
height: this.height,
|
|
252
|
+
get: mask
|
|
253
|
+
} : mask;
|
|
249
254
|
const cachedSource = new GridBase(source);
|
|
250
255
|
this.batchUpdate(() => {
|
|
251
256
|
for (let oy = 0; oy < source.height; oy++) {
|
|
252
257
|
for (let ox = 0; ox < source.width; ox++) {
|
|
253
258
|
const tx = ox + x;
|
|
254
259
|
const ty = oy + y;
|
|
255
|
-
if (
|
|
260
|
+
if (maskSource && !maskSource.get(tx, ty))
|
|
256
261
|
continue;
|
|
257
262
|
this.trySet(tx, ty, cachedSource.get(ox, oy));
|
|
258
263
|
}
|
package/lib/index.d.ts
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
export { Grid, type GridBase, type Cell, TraversalType
|
|
1
|
+
export { Grid, type GridBase, type Cell, TraversalType } from "./grid.js";
|
package/lib/index.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
export { Grid, TraversalType
|
|
1
|
+
export { Grid, TraversalType } from "./grid.js";
|
package/package.json
CHANGED