@xtia/grid 0.2.0 → 0.4.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 CHANGED
@@ -22,7 +22,7 @@ npm i @xtia/grid
22
22
  ```
23
23
 
24
24
  ```ts
25
- import { Grid } from "@xtia/grid"; // ~4.8kb gzipped (2.2kb + Pipe2D dep)
25
+ import { Grid } from "@xtia/grid"; // ~5.1kb gzipped (2.5kb + Pipe2D dep)
26
26
 
27
27
  // initialise a 30x20 Grid<number> of 0's
28
28
  const numGrid = Grid.solid(30, 20, 0);
@@ -115,11 +115,11 @@ spriteMap.set(1, 0, "forest.png"); // modify the view
115
115
  console.log(world.get(1, 0)); // read the parent: "forest"
116
116
  ```
117
117
 
118
- For one-way read-mapping, use `grid.pipe` - a [Pipe2D](https://github.com/tiadrop/pipe2d) view into the grid's data.
118
+ For one-way read-mapping, use `grid.values` - a [Pipe2D](https://github.com/tiadrop/pipe2d) view into the grid's data.
119
119
 
120
120
  ```ts
121
121
  // create a one-way, player-centred sprite map
122
- const viewport = world.pipe
122
+ const viewport = world.values
123
123
  .oob("mountain")
124
124
  .map(t => t + ".png")
125
125
  .crop(playerX - 5, playerY - 5, 10, 10);
@@ -138,7 +138,7 @@ const colourMap = new Map(Object.entries({
138
138
  forest: C.x080,
139
139
  mountain: C.xa70
140
140
  }));
141
- const minimap = world.pipe
141
+ const minimap = world.values
142
142
  .map(colourMap)
143
143
  .floorCoordinates()
144
144
  .crop(viewX, viewY, vw, vh)
@@ -236,7 +236,7 @@ We can perform batched updates, suppressing the 'change' event until a process c
236
236
  Pipe2D makes it easy to save and restore grid data:
237
237
 
238
238
  ```ts
239
- const snapshot = world.pipe.stash();
239
+ const snapshot = world.values.stash();
240
240
 
241
241
  const restored = Grid.init(snapshot);
242
242
  // or paste it to an existing grid
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;
@@ -57,8 +82,9 @@ export declare class Grid<T> {
57
82
  private parentGet;
58
83
  private parentSet;
59
84
  private batch;
60
- readonly cells: Pipe2D<Cell<T>>;
61
85
  protected constructor(width: number, height: number, parentGet: GetXYFunc<T>, parentSet: (x: number, y: number, value: T) => void, batch: (cb: () => void) => void);
86
+ readonly cells: Pipe2D<Cell<T>>;
87
+ values: Pipe2D<T>;
62
88
  pipe: Pipe2D<T>;
63
89
  batchUpdate(fn: () => void): void;
64
90
  get(x: number, y: number): T;
@@ -66,16 +92,19 @@ export declare class Grid<T> {
66
92
  trySet(x: number, y: number, value: T): boolean;
67
93
  fill(value: T): void;
68
94
  paste(source: Source2D<T>, x?: number, y?: number): void;
95
+ paste(get: GetXYFunc<T>): void;
69
96
  writeMask(mask: Source2D<boolean> | GetXYFunc<boolean>): Grid<T>;
70
97
  [Symbol.iterator](): IterableIterator<T>;
71
98
  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>;
99
+ map<U>(read: (initial: T, x: number, y: number) => U, write: ((local: U, x: number, y: number) => T)): Grid<U>;
73
100
  region(x: number, y: number, width: number, height: number): Grid<T>;
101
+ region(rect: RectSpec): Grid<T>;
74
102
  scroll(xDelta: number, yDelta: number, fill: T): this;
75
103
  static init<T>(width: number, height: number, initCell: (x: number, y: number) => T): GridBase<T>;
76
104
  static init<T>(source: Source2D<T>): GridBase<T>;
77
105
  static solid<T>(width: number, height: number, fillValue: T): GridBase<T>;
78
106
  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>;
107
+ private assertValidCoordinates;
79
108
  }
80
109
  export declare class GridBase<T> extends Grid<T> {
81
110
  private data;
package/lib/grid.js CHANGED
@@ -6,7 +6,12 @@ export var TraversalType;
6
6
  TraversalType[TraversalType["diagonal"] = 1] = "diagonal";
7
7
  TraversalType[TraversalType["shortcut"] = 2] = "shortcut";
8
8
  })(TraversalType || (TraversalType = {}));
9
+ const cardinalOffsets = [[-1, 0], [1, 0], [0, -1], [0, 1]];
10
+ const allOffsets = [[-1, -1], [-1, 0], [-1, 1], [0, -1], [0, 1], [1, -1], [1, 0], [1, 1]];
9
11
  export class Cell {
12
+ x;
13
+ y;
14
+ gridView;
10
15
  constructor(x, y, gridView) {
11
16
  this.x = x;
12
17
  this.y = y;
@@ -36,12 +41,11 @@ export class Cell {
36
41
  }
37
42
  getNeighbours(includeDiagonals = false) {
38
43
  const offsets = includeDiagonals
39
- ? [[-1, -1], [-1, 0], [-1, 1], [0, -1], [0, 1], [1, -1], [1, 0], [1, 1]]
40
- : [[-1, 0], [1, 0], [0, -1], [0, 1]];
44
+ ? allOffsets
45
+ : cardinalOffsets;
41
46
  return offsets
42
- .map(([ox, oy]) => [ox + this.x, oy + this.y])
43
- .filter(([tx, ty]) => tx >= 0 && tx < this.gridView.width && ty >= 0 && ty < this.gridView.height)
44
- .map(([tx, ty]) => this.gridView.cells.get(tx, ty));
47
+ .map(o => this.look(o[0], o[1]))
48
+ .filter(v => v);
45
49
  }
46
50
  look(xDelta, yDelta) {
47
51
  const [x, y] = [this.x + xDelta, this.y + yDelta];
@@ -50,8 +54,11 @@ export class Cell {
50
54
  return this.gridView.cells.get(xDelta + this.x, yDelta + this.y);
51
55
  }
52
56
  findPath(target, getCost, options = {}) {
53
- const pathMap = this.calculateCosts(getCost, Object.assign(Object.assign({}, options), { stopAt: target }), true);
54
- return pathMap === null || pathMap === void 0 ? void 0 : pathMap.get(...this.getSiblingXY(target));
57
+ const pathMap = this.calculateCosts(getCost, {
58
+ ...options,
59
+ stopAt: target,
60
+ }, true);
61
+ return pathMap?.get(...this.getSiblingXY(target));
55
62
  }
56
63
  getPathMap(getCost, options = {}) {
57
64
  return this.calculateCosts(getCost, options, true);
@@ -60,16 +67,14 @@ export class Cell {
60
67
  return this.calculateCosts(getCost, options);
61
68
  }
62
69
  calculateCosts(getCost, options, asPathMap = false) {
63
- var _a, _b;
64
- var _c;
65
70
  const visited = new Set();
66
71
  const costs = new Map();
67
- const maxCost = (_c = options.maxCost) !== null && _c !== void 0 ? _c : Infinity;
72
+ const maxCost = options.maxCost ?? Infinity;
68
73
  const pathInfo = asPathMap
69
74
  ? new Map()
70
75
  : null;
71
76
  const getCostFn = getCost instanceof Map
72
- ? (cell) => { var _a; return (_a = getCost.get(cell.value)) !== null && _a !== void 0 ? _a : Infinity; }
77
+ ? (cell) => getCost.get(cell.value) ?? Infinity
73
78
  : getCost;
74
79
  costs.set(this, 0);
75
80
  const queue = new OrderedQueue([this, 0]);
@@ -79,7 +84,7 @@ export class Cell {
79
84
  if (visited.has(current))
80
85
  continue;
81
86
  visited.add(current);
82
- (_a = options.onVisit) === null || _a === void 0 ? void 0 : _a.call(options, { cell: current, cost: costs.get(current) });
87
+ options.onVisit?.({ cell: current, cost: costs.get(current) });
83
88
  if (stopAtCell && current === stopAtCell) {
84
89
  break;
85
90
  }
@@ -92,7 +97,7 @@ export class Cell {
92
97
  }));
93
98
  const shortcutCells = typeof options.shortcutMap == "function"
94
99
  ? options.shortcutMap(current.x, current.y)
95
- : (_b = options.shortcutMap) === null || _b === void 0 ? void 0 : _b.get(current.x, current.y);
100
+ : options.shortcutMap?.get(current.x, current.y);
96
101
  if (shortcutCells) {
97
102
  const shortcuts = shortcutCells.map(loc => this.getSiblingCell(loc)).map(cell => ({
98
103
  traversalType: TraversalType.shortcut,
@@ -101,7 +106,6 @@ export class Cell {
101
106
  neighbours.push(...shortcuts);
102
107
  }
103
108
  neighbours.forEach(n => {
104
- var _a;
105
109
  if (visited.has(n.cell))
106
110
  return;
107
111
  const moveCost = getCostFn(n.cell, {
@@ -111,10 +115,10 @@ export class Cell {
111
115
  if (moveCost === Infinity)
112
116
  return;
113
117
  const newCost = currentCost + moveCost;
114
- const oldCost = (_a = costs.get(n.cell)) !== null && _a !== void 0 ? _a : Infinity;
118
+ const oldCost = costs.get(n.cell) ?? Infinity;
115
119
  if (newCost < oldCost && newCost <= maxCost) {
116
120
  costs.set(n.cell, newCost);
117
- pathInfo === null || pathInfo === void 0 ? void 0 : pathInfo.set(n.cell, current);
121
+ pathInfo?.set(n.cell, current);
118
122
  queue.add(n.cell, newCost);
119
123
  }
120
124
  });
@@ -181,9 +185,8 @@ export class Cell {
181
185
  }
182
186
  }
183
187
  createVisibilityMap(isClear, options = {}) {
184
- var _a;
185
188
  const maxDistance = options.maxDistance;
186
- const boundariesVisible = (_a = options.boundariesVisible) !== null && _a !== void 0 ? _a : true;
189
+ const boundariesVisible = options.boundariesVisible ?? true;
187
190
  return new Pipe2D(this.gridView.width, this.gridView.height, (x, y) => {
188
191
  if (!isClear(this)) {
189
192
  return x === this.x && y === this.y ? boundariesVisible : false;
@@ -208,76 +211,73 @@ function getDistance(c1, c2) {
208
211
  return Math.sqrt(Math.pow(c2.x - c1.x, 2) + Math.pow(c2.y - c1.y, 2));
209
212
  }
210
213
  export class Grid {
214
+ width;
215
+ height;
216
+ parentGet;
217
+ parentSet;
218
+ batch;
211
219
  constructor(width, height, parentGet, parentSet, batch) {
212
220
  this.width = width;
213
221
  this.height = height;
214
222
  this.parentGet = parentGet;
215
223
  this.parentSet = parentSet;
216
224
  this.batch = batch;
217
- this.cells = new Pipe2D(this.width, this.height, (x, y) => {
218
- if (x < 0 || x >= this.width || y < 0 || y >= this.height || !Number.isInteger(x) || !Number.isInteger(y)) {
225
+ if (!Number.isInteger(width) || !Number.isInteger(height) || width < 0 || height < 0) {
226
+ throw new Error(`Grid width & height must be non-negative integers; got (${width} x ${height})`);
227
+ }
228
+ this.cells = new Pipe2D(width, height, (x, y) => {
229
+ if (x < 0 || x >= width || y < 0 || y >= height || !Number.isInteger(x) || !Number.isInteger(y)) {
219
230
  throw new RangeError(`Cell coordinates must be bounded integers; got (${x}, ${y})`);
220
231
  }
221
232
  return new Cell(x, y, this);
222
- }).strict().withCache();
223
- this.pipe = new Pipe2D(this);
224
- if (!Number.isInteger(width) || !Number.isInteger(height)) {
225
- throw new Error(`Grid width & height must be integer; got (${width} x ${height})`);
226
- }
227
- if (width < 0 || height < 0) {
228
- throw new Error(`Grid width/height may not be less than 0; got (${width} x ${height})`);
229
- }
233
+ }).withCache();
230
234
  }
235
+ cells;
236
+ values = new Pipe2D(this);
237
+ pipe = this.values;
231
238
  batchUpdate(fn) {
232
239
  this.batch(fn);
233
240
  }
234
241
  get(x, y) {
235
- if (!Number.isInteger(x) || !Number.isInteger(y)) {
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
- }
242
+ this.assertValidCoordinates(x, y);
241
243
  return this.parentGet(x, y);
242
244
  }
243
245
  set(x, y, value) {
244
- if (!Number.isInteger(x) || !Number.isInteger(y)) {
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
- }
246
+ this.assertValidCoordinates(x, y);
250
247
  this.parentSet(x, y, value);
251
248
  }
252
249
  trySet(x, y, value) {
253
250
  if (x < 0
254
251
  || x >= this.width
255
252
  || y < 0
256
- || y >= this.height
257
- || !Number.isInteger(x)
258
- || !Number.isInteger(y))
253
+ || y >= this.height)
259
254
  return false;
260
- this.set(x, y, value);
255
+ this.parentSet(x, y, value);
261
256
  return true;
262
257
  }
263
258
  fill(value) {
264
259
  this.batchUpdate(() => {
265
260
  for (let y = 0; y < this.height; y++) {
266
261
  for (let x = 0; x < this.width; x++) {
267
- this.set(x, y, value);
262
+ this.parentSet(x, y, value);
268
263
  }
269
264
  }
270
265
  });
271
266
  }
272
- paste(source, x = 0, y = 0) {
273
- const cachedSource = new Pipe2D(source).stash();
267
+ paste(_source, x = 0, y = 0) {
268
+ this.assertValidCoordinates(x, y, true);
269
+ const source = typeof _source == "function"
270
+ ? new Pipe2D(this.width, this.height, _source)
271
+ : _source;
272
+ const sourcePipe = source instanceof Pipe2D ? source : new Pipe2D(source);
273
+ const cachedSource = sourcePipe.stash();
274
274
  this.batchUpdate(() => {
275
275
  for (let oy = 0; oy < source.height; oy++) {
276
276
  for (let ox = 0; ox < source.width; ox++) {
277
277
  const tx = ox + x, ty = oy + y;
278
278
  if (tx < 0 || tx >= this.width || ty < 0 || ty >= this.height)
279
279
  continue;
280
- this.set(tx, ty, cachedSource.get(ox, oy));
280
+ this.parentSet(tx, ty, cachedSource.get(ox, oy));
281
281
  }
282
282
  }
283
283
  });
@@ -287,10 +287,15 @@ export class Grid {
287
287
  ? mask
288
288
  : (x, y) => mask.get(x, y);
289
289
  return new Grid(this.width, this.height, this.parentGet, (x, y, v) => {
290
- const writable = getMask(x, y);
291
- if (writable) {
292
- this.set(x, y, v);
290
+ try {
291
+ const writable = getMask(x, y);
292
+ if (!writable)
293
+ return;
294
+ }
295
+ catch (e) {
296
+ throw new Error(`Failed to read write-mask at (${x}, ${y})`, { cause: e });
293
297
  }
298
+ this.parentSet(x, y, v);
294
299
  }, cb => this.batch(cb));
295
300
  }
296
301
  *[Symbol.iterator]() {
@@ -310,12 +315,39 @@ export class Grid {
310
315
  map(read, write) {
311
316
  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
317
  }
313
- region(x, y, width, height) {
314
- if (!Number.isInteger(x) || !Number.isInteger(y)) {
315
- throw new Error(`Region boundaries must be integer; got (${x}, ${y}), (${width} x ${height})`);
318
+ region(xOrRect, y = 0, width = 0, height = 0) {
319
+ let x;
320
+ if (typeof xOrRect == "number") {
321
+ x = xOrRect;
322
+ }
323
+ else {
324
+ if (xOrRect.left === undefined) {
325
+ width = xOrRect.width;
326
+ x = (this.width - 1 - xOrRect.right) - width + 1;
327
+ }
328
+ else {
329
+ x = xOrRect.left;
330
+ width = xOrRect.width === undefined
331
+ ? (this.width - 1 - xOrRect.right) - x + 1
332
+ : xOrRect.width;
333
+ }
334
+ if (xOrRect.top === undefined) {
335
+ height = xOrRect.height;
336
+ y = (this.height - 1 - xOrRect.bottom) - height + 1;
337
+ }
338
+ else {
339
+ y = xOrRect.top;
340
+ height = xOrRect.height === undefined
341
+ ? (this.height - 1 - xOrRect.bottom) - y + 1
342
+ : xOrRect.height;
343
+ }
316
344
  }
317
- if (x < 0 || y < 0 || x + width > this.width || y + height > this.height) {
318
- throw new Error("Requested region exceeds the parent's bounds");
345
+ this.assertValidCoordinates(x, y);
346
+ if (!Number.isInteger(width) || !Number.isInteger(height)) {
347
+ throw new Error("Region dimensions must be integer");
348
+ }
349
+ if (x + width > this.width || y + height > this.height) {
350
+ throw new RangeError("Requested region exceeds the parent's bounds");
319
351
  }
320
352
  return new Grid(width, height, (tx, ty) => {
321
353
  return this.parentGet(tx + x, ty + y);
@@ -324,9 +356,7 @@ export class Grid {
324
356
  }, fn => this.batch(fn));
325
357
  }
326
358
  scroll(xDelta, yDelta, fill) {
327
- if (!Number.isInteger(xDelta) || !Number.isInteger(yDelta)) {
328
- throw new Error(`Scroll offsets must be integer; got (${xDelta}, ${yDelta})`);
329
- }
359
+ this.assertValidCoordinates(xDelta, yDelta, true);
330
360
  this.paste(this, xDelta, yDelta);
331
361
  if (xDelta > 0) {
332
362
  this.region(0, 0, xDelta, this.height).fill(fill);
@@ -357,12 +387,22 @@ export class Grid {
357
387
  static wrap(width, height, get, set, batch = cb => cb()) {
358
388
  return new Grid(width, height, get, set, batch);
359
389
  }
390
+ assertValidCoordinates(x, y, ignoreBounds = false) {
391
+ if (!Number.isInteger(x) || !Number.isInteger(y)) {
392
+ throw new Error(`Coordinates must be integer; got (${x}, ${y})`);
393
+ }
394
+ if (!ignoreBounds && (x < 0 || x >= this.width || y < 0 || y >= this.height)) {
395
+ throw new RangeError(`Coordinates out of bounds (${x}, ${y}) / (${this.width}, ${this.height})`);
396
+ }
397
+ }
360
398
  }
361
399
  export class GridBase extends Grid {
400
+ data;
401
+ eventHandlers = {};
362
402
  triggerEvent(name, data) {
363
- var _a;
364
- (_a = this.eventHandlers[name]) === null || _a === void 0 ? void 0 : _a.forEach(obj => obj.fn(data));
403
+ this.eventHandlers[name]?.forEach(obj => obj.fn(data));
365
404
  }
405
+ batchState = null;
366
406
  batchUpdate(fn) {
367
407
  if (this.batchState) {
368
408
  this.batchState.level++;
@@ -384,8 +424,6 @@ export class GridBase extends Grid {
384
424
  }
385
425
  constructor(source) {
386
426
  super(source.width, source.height, (x, y) => this.data[y * this.width + x], (x, y, value) => this._set(x, y, value), fn => this.batchUpdate(fn));
387
- this.eventHandlers = {};
388
- this.batchState = null;
389
427
  const sourcePipe = source instanceof Pipe2D
390
428
  ? source
391
429
  : new Pipe2D(source);
package/lib/utils.js CHANGED
@@ -1,6 +1,6 @@
1
1
  export class OrderedQueue {
2
+ queue = [];
2
3
  constructor(...items) {
3
- this.queue = [];
4
4
  items.forEach(v => this.add(...v));
5
5
  }
6
6
  add(value, priority) {
package/package.json CHANGED
@@ -1,5 +1,5 @@
1
1
  {
2
- "version": "0.2.0",
2
+ "version": "0.4.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",