@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 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(isWall: (cell: Cell<T>) => boolean, maxDistance?: number): Pipe2D<Visibility>;
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(isWall, maxDistance = Infinity) {
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 (isWall(this))
181
- return Visibility.hidden;
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
- for (const cell of this.getLineTo({ x, y })) {
187
- if (isWall(cell)) {
188
- if (cell.x === this.x && cell.y === this.y) {
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
- return Visibility.visible;
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 (mask && !mask.get(x, y))
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 (mask && !mask.get(tx, ty))
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, Visibility } from "./grid.js";
1
+ export { Grid, type GridBase, type Cell, TraversalType } from "./grid.js";
package/lib/index.js CHANGED
@@ -1 +1 @@
1
- export { Grid, TraversalType, Visibility } from "./grid.js";
1
+ export { Grid, TraversalType } from "./grid.js";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
- "version": "0.0.2",
3
- "description": "2D store and utilities",
2
+ "version": "0.0.4",
3
+ "description": "2D store and utilities, with zero-copy regions and transform layers, pathfinding, Pipe2D interop",
4
4
  "name": "@xtia/grid",
5
5
  "exports": {
6
6
  ".": {