@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 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 integration** - materialise pipes, get pipe views
15
- - **Consistent API** - for grids and regions
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"; // ~5.3kb gzipped
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 live-view interfaces relating to positions in the grid.
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 `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.
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>`, 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)`.
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
- You can perform batched updates, holding off the 'change' event until the batch process concludes, with `grid.batchUpdate(callback)`.
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?: [number, number] | Cell<T>;
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: Cell<T> | [number, number], getCost: CostFunc<T>, options?: PathFindOptions<T>): Cell<T>[] | null;
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
- x: number;
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: targetXY,
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(...targetXY);
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 targetCell = Array.isArray(options.stopAt)
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 (targetCell && current === targetCell) {
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 ? TraversalType.cardinal : TraversalType.diagonal,
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(target) {
139
- const [targetX, targetY] = Array.isArray(target)
140
- ? target
141
- : [target.x, target.y];
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({ x, y })) {
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.5",
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"