@xtia/grid 0.1.1 → 0.3.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
@@ -43,7 +43,7 @@ const source = imagePipe
43
43
  .crop(10, 10, 64, 64)
44
44
  .scale(.5)
45
45
  .rotateLeft();
46
- const grid = Grid.from(source);
46
+ const grid = Grid.init(source);
47
47
  ```
48
48
 
49
49
  ### Wrapping existing structures
@@ -97,7 +97,7 @@ world.writeMask(mask).fill("mountain");
97
97
  const lakeRegion = world.region(25, 25, 30, 30);
98
98
 
99
99
  // do we want a distinct, self-contained copy of the region?
100
- const lakeGrid = Grid.from(lakeRegion);
100
+ const lakeGrid = Grid.init(lakeRegion);
101
101
  ```
102
102
 
103
103
  ## Transformation Layers
@@ -225,9 +225,9 @@ Unlike visibility maps, which are lazily evaluated according to the supplied `is
225
225
 
226
226
  ## The storage layer
227
227
 
228
- `Grid` itself is an interface for reading and writing 2D data. `GridBase` - a subclass of `Grid` - maintains the actual storage of such data.
228
+ `Grid` itself is an interface that sits on top of a synchronous 2D read/write mechanism. `GridBase` is a subclass of `Grid` with built-in storage.
229
229
 
230
- 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)`.
230
+ Although the factory methods `Grid.solid<T>()` and `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)`.
231
231
 
232
232
  We can perform batched updates, suppressing the 'change' event until a process concludes, with `grid.batchUpdate(callback)`.
233
233
 
@@ -238,7 +238,7 @@ Pipe2D makes it easy to save and restore grid data:
238
238
  ```ts
239
239
  const snapshot = world.pipe.stash();
240
240
 
241
- const restored = Grid.from(snapshot);
241
+ const restored = Grid.init(snapshot);
242
242
  // or paste it to an existing grid
243
243
  world.paste(snapshot);
244
244
  ```
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;
@@ -69,13 +94,15 @@ export declare class Grid<T> {
69
94
  writeMask(mask: Source2D<boolean> | GetXYFunc<boolean>): Grid<T>;
70
95
  [Symbol.iterator](): IterableIterator<T>;
71
96
  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>;
97
+ map<U>(read: (initial: T, x: number, y: number) => U, write: ((local: U, x: number, y: number) => T)): Grid<U>;
73
98
  region(x: number, y: number, width: number, height: number): Grid<T>;
99
+ region(rect: RectSpec): Grid<T>;
74
100
  scroll(xDelta: number, yDelta: number, fill: T): this;
75
101
  static init<T>(width: number, height: number, initCell: (x: number, y: number) => T): GridBase<T>;
76
- static from<T>(source: Source2D<T>): GridBase<T>;
102
+ static init<T>(source: Source2D<T>): GridBase<T>;
77
103
  static solid<T>(width: number, height: number, fillValue: T): GridBase<T>;
78
104
  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>;
105
+ private assertValidCoordinates;
79
106
  }
80
107
  export declare class GridBase<T> extends Grid<T> {
81
108
  private data;
package/lib/grid.js CHANGED
@@ -219,65 +219,52 @@ export class Grid {
219
219
  throw new RangeError(`Cell coordinates must be bounded integers; got (${x}, ${y})`);
220
220
  }
221
221
  return new Cell(x, y, this);
222
- }).strict().withCache();
222
+ }).withCache();
223
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})`);
224
+ if (!Number.isInteger(width) || !Number.isInteger(height) || width < 0 || height < 0) {
225
+ throw new Error(`Grid width & height must be non-negative integers; got (${width} x ${height})`);
229
226
  }
230
227
  }
231
228
  batchUpdate(fn) {
232
229
  this.batch(fn);
233
230
  }
234
231
  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
- }
232
+ this.assertValidCoordinates(x, y);
241
233
  return this.parentGet(x, y);
242
234
  }
243
235
  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
- }
236
+ this.assertValidCoordinates(x, y);
250
237
  this.parentSet(x, y, value);
251
238
  }
252
239
  trySet(x, y, value) {
253
240
  if (x < 0
254
241
  || x >= this.width
255
242
  || y < 0
256
- || y >= this.height
257
- || !Number.isInteger(x)
258
- || !Number.isInteger(y))
243
+ || y >= this.height)
259
244
  return false;
260
- this.set(x, y, value);
245
+ this.parentSet(x, y, value);
261
246
  return true;
262
247
  }
263
248
  fill(value) {
264
249
  this.batchUpdate(() => {
265
250
  for (let y = 0; y < this.height; y++) {
266
251
  for (let x = 0; x < this.width; x++) {
267
- this.set(x, y, value);
252
+ this.parentSet(x, y, value);
268
253
  }
269
254
  }
270
255
  });
271
256
  }
272
257
  paste(source, x = 0, y = 0) {
273
- const cachedSource = new Pipe2D(source).stash();
258
+ this.assertValidCoordinates(x, y, true);
259
+ const sourcePipe = source instanceof Pipe2D ? source : new Pipe2D(source);
260
+ const cachedSource = sourcePipe.stash();
274
261
  this.batchUpdate(() => {
275
262
  for (let oy = 0; oy < source.height; oy++) {
276
263
  for (let ox = 0; ox < source.width; ox++) {
277
264
  const tx = ox + x, ty = oy + y;
278
265
  if (tx < 0 || tx >= this.width || ty < 0 || ty >= this.height)
279
266
  continue;
280
- this.set(tx, ty, cachedSource.get(ox, oy));
267
+ this.parentSet(tx, ty, cachedSource.get(ox, oy));
281
268
  }
282
269
  }
283
270
  });
@@ -289,7 +276,7 @@ export class Grid {
289
276
  return new Grid(this.width, this.height, this.parentGet, (x, y, v) => {
290
277
  const writable = getMask(x, y);
291
278
  if (writable) {
292
- this.set(x, y, v);
279
+ this.parentSet(x, y, v);
293
280
  }
294
281
  }, cb => this.batch(cb));
295
282
  }
@@ -310,12 +297,39 @@ export class Grid {
310
297
  map(read, write) {
311
298
  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
299
  }
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})`);
300
+ region(xOrRect, y = 0, width = 0, height = 0) {
301
+ let x;
302
+ if (typeof xOrRect == "number") {
303
+ x = xOrRect;
316
304
  }
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");
305
+ else {
306
+ if (xOrRect.left === undefined) {
307
+ width = xOrRect.width;
308
+ x = (this.width - 1 - xOrRect.right) - width + 1;
309
+ }
310
+ else {
311
+ x = xOrRect.left;
312
+ width = xOrRect.width === undefined
313
+ ? (this.width - 1 - xOrRect.right) - x + 1
314
+ : xOrRect.width;
315
+ }
316
+ if (xOrRect.top === undefined) {
317
+ height = xOrRect.height;
318
+ y = (this.height - 1 - xOrRect.bottom) - height + 1;
319
+ }
320
+ else {
321
+ y = xOrRect.top;
322
+ height = xOrRect.height === undefined
323
+ ? (this.height - 1 - xOrRect.bottom) - y + 1
324
+ : xOrRect.height;
325
+ }
326
+ }
327
+ this.assertValidCoordinates(x, y);
328
+ if (!Number.isInteger(width) || !Number.isInteger(height)) {
329
+ throw new Error("Region dimensions must be integer");
330
+ }
331
+ if (x + width > this.width || y + height > this.height) {
332
+ throw new RangeError("Requested region exceeds the parent's bounds");
319
333
  }
320
334
  return new Grid(width, height, (tx, ty) => {
321
335
  return this.parentGet(tx + x, ty + y);
@@ -324,9 +338,7 @@ export class Grid {
324
338
  }, fn => this.batch(fn));
325
339
  }
326
340
  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
- }
341
+ this.assertValidCoordinates(xDelta, yDelta, true);
330
342
  this.paste(this, xDelta, yDelta);
331
343
  if (xDelta > 0) {
332
344
  this.region(0, 0, xDelta, this.height).fill(fill);
@@ -342,22 +354,29 @@ export class Grid {
342
354
  }
343
355
  return this;
344
356
  }
345
- static init(width, height, initCell) {
357
+ static init(widthOrSource, height, initCell) {
358
+ if (typeof widthOrSource == "object")
359
+ return new GridBase(widthOrSource);
346
360
  return new GridBase({
347
- width,
348
- height,
361
+ width: widthOrSource,
362
+ height: height,
349
363
  get: initCell
350
364
  });
351
365
  }
352
- static from(source) {
353
- return new GridBase(source);
354
- }
355
366
  static solid(width, height, fillValue) {
356
367
  return new GridBase(Pipe2D.solid(fillValue, width, height));
357
368
  }
358
369
  static wrap(width, height, get, set, batch = cb => cb()) {
359
370
  return new Grid(width, height, get, set, batch);
360
371
  }
372
+ assertValidCoordinates(x, y, ignoreBounds = false) {
373
+ if (!Number.isInteger(x) || !Number.isInteger(y)) {
374
+ throw new Error(`Coordinates must be integer; got (${x}, ${y})`);
375
+ }
376
+ if (!ignoreBounds && (x < 0 || x >= this.width || y < 0 || y >= this.height)) {
377
+ throw new RangeError(`Coordinates out of bounds (${x}, ${y}) / (${this.width}, ${this.height})`);
378
+ }
379
+ }
361
380
  }
362
381
  export class GridBase extends Grid {
363
382
  triggerEvent(name, data) {
package/package.json CHANGED
@@ -1,5 +1,5 @@
1
1
  {
2
- "version": "0.1.1",
2
+ "version": "0.3.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",