@xtia/grid 0.0.1 → 0.0.3
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 +156 -0
- package/lib/grid.d.ts +6 -20
- package/lib/grid.js +40 -121
- package/package.json +5 -7
package/README.md
ADDED
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
# `Grid`
|
|
2
|
+
|
|
3
|
+
## Summary
|
|
4
|
+
|
|
5
|
+
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.
|
|
6
|
+
|
|
7
|
+
## Features
|
|
8
|
+
|
|
9
|
+
- **Mutable 2D storage** with type safety
|
|
10
|
+
- **Zero-copy region views** - edit or provide subgrids and transformation layers without copying
|
|
11
|
+
- **Spatial operations** - fill, paste, flood fill, pathfinding
|
|
12
|
+
- **Pipe2D integration** - materialise pipes, get pipe views
|
|
13
|
+
- **Consistent API** - for grids and regions
|
|
14
|
+
- **Mask support** - apply operations to arbitrary shapes
|
|
15
|
+
|
|
16
|
+
## Example
|
|
17
|
+
|
|
18
|
+
```
|
|
19
|
+
npm i @xtia/grid
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
```ts
|
|
23
|
+
import { Grid } from "@xtia/grid";
|
|
24
|
+
|
|
25
|
+
// initialise a 30x20 Grid<number> of 0's
|
|
26
|
+
const numGrid = Grid.solid(30, 20, 0);
|
|
27
|
+
|
|
28
|
+
// change a value
|
|
29
|
+
numGrid.set(3, 3, 50);
|
|
30
|
+
|
|
31
|
+
// initialise a chessboard
|
|
32
|
+
const chessGrid = Grid.init(8, 8, (x, y) =>
|
|
33
|
+
(x + y) % 2 === 0 ? 'black' : 'white'
|
|
34
|
+
);
|
|
35
|
+
|
|
36
|
+
// read a value
|
|
37
|
+
const colour = chessGrid.get(4, 5);
|
|
38
|
+
|
|
39
|
+
// initialise a grid from a Pipe2D
|
|
40
|
+
const source = imagePipe
|
|
41
|
+
.crop(10, 10, 64, 64)
|
|
42
|
+
.scale(.5)
|
|
43
|
+
.rotateLeft();
|
|
44
|
+
const grid = Grid.from(source);
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
## Regions
|
|
48
|
+
|
|
49
|
+
Use `grid.region(x, y, w, h)` to define a subgrid. The subgrid is a **view** into the parent; changes to the subgrid affect the parent and vice-versa.
|
|
50
|
+
|
|
51
|
+
```ts
|
|
52
|
+
// game map with terrain
|
|
53
|
+
const world = Grid.solid(100, 100, 0); // 0 = grass
|
|
54
|
+
|
|
55
|
+
// add a lake
|
|
56
|
+
world.region(30, 30, 20, 20).fill(1); // 1 = water
|
|
57
|
+
|
|
58
|
+
// add mountains around the lake with a circular mask
|
|
59
|
+
const centre = world.cells.get(40, 40);
|
|
60
|
+
const mask = (x: number, y: number) => {
|
|
61
|
+
const dist = Math.hypot(x - centre.x, y - centre.y);
|
|
62
|
+
return dist > 12 && dist < 18; // mountain ring
|
|
63
|
+
};
|
|
64
|
+
world.fill(2, mask); // 2 = mountain
|
|
65
|
+
|
|
66
|
+
// get a view of just the interesting area
|
|
67
|
+
const lakeRegion = world.region(25, 25, 30, 30);
|
|
68
|
+
|
|
69
|
+
// do we want a distinct, self-contained copy of the region?
|
|
70
|
+
const lakeGrid = Grid.from(lakeRegion);
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
## Transformation Layers
|
|
74
|
+
|
|
75
|
+
`grid.map(read, write)` creates a **view** that reads and writes the parent through the provided transformation functions.
|
|
76
|
+
|
|
77
|
+
```ts
|
|
78
|
+
const sprites = ["grass.png", "water.png", "mountain.png", "forest.png"];
|
|
79
|
+
const spriteMap = world.map(
|
|
80
|
+
n => sprites[n],
|
|
81
|
+
s => sprites.indexOf(s)
|
|
82
|
+
);
|
|
83
|
+
|
|
84
|
+
world.set(0, 0, 2); // modify the parent
|
|
85
|
+
console.log(spriteMap.get(0, 0)); // read the transformed view: "mountain.png"
|
|
86
|
+
spriteMap.set(1, 0, "forest.png"); // modify the view
|
|
87
|
+
console.log(world.get(1, 0)); // read the parent: 3
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
For one-way read-mapping, use `grid.pipe` - a [Pipe2D](https://github.com/tiadrop/pipe2d) view into the grid's data.
|
|
91
|
+
|
|
92
|
+
## Cell interface
|
|
93
|
+
|
|
94
|
+
`grid.cells` provides a Pipe2D, for convenient transformation, of live-view interfaces relating to positions in the grid.
|
|
95
|
+
|
|
96
|
+
```ts
|
|
97
|
+
const topLeft = world.cells.get(0, 0);
|
|
98
|
+
console.log(topLeft.value); // 2
|
|
99
|
+
|
|
100
|
+
topLeft.value = 3; // modifies the underlying data
|
|
101
|
+
console.log(spriteMap.get(0, 0)); // now "forest.png";
|
|
102
|
+
|
|
103
|
+
// navigate by offset
|
|
104
|
+
const adjacentCell = topLeft.look(1, 0);
|
|
105
|
+
console.log(adjacentCell.x, adjacentCell.y); // 1, 0
|
|
106
|
+
```
|
|
107
|
+
|
|
108
|
+
Cells provide methods for locational utilities such as pathfinding and visibility mapping.
|
|
109
|
+
|
|
110
|
+
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.
|
|
111
|
+
|
|
112
|
+
### Path finding
|
|
113
|
+
```
|
|
114
|
+
const costs = [1, Infinity, Infinity, 2]; // grass=1, water/mountain=impassable, forest=2
|
|
115
|
+
|
|
116
|
+
const start = world.cells.get(5, 5);
|
|
117
|
+
const destination = world.cells.get(90, 90);
|
|
118
|
+
const path = start.findPath(
|
|
119
|
+
destination,
|
|
120
|
+
(cell) => costs[cell.value]
|
|
121
|
+
); // Cell<number>[]
|
|
122
|
+
```
|
|
123
|
+
|
|
124
|
+
### Visibility mapping
|
|
125
|
+
|
|
126
|
+
```ts
|
|
127
|
+
const visibility = start.createVisibilityMap(
|
|
128
|
+
cell => cell.value == 0 // anything except grass blocks vision
|
|
129
|
+
);
|
|
130
|
+
|
|
131
|
+
// any 2d source, including visibility maps, can be used as a mask
|
|
132
|
+
// paste(source: Source2D<T>, mask?: Source2D<boolean>)
|
|
133
|
+
screenGrid.paste(spriteMap, visibility.map(v => v != Visibility.hidden));
|
|
134
|
+
|
|
135
|
+
// using pipe2d's convenience
|
|
136
|
+
const mountainCells = world.cells.toFlatArrayXY().filter(c => c.value === 2);
|
|
137
|
+
```
|
|
138
|
+
|
|
139
|
+
### Reusing path data
|
|
140
|
+
|
|
141
|
+
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 form that data (and cached) when that pipe is queried.
|
|
142
|
+
|
|
143
|
+
```ts
|
|
144
|
+
const pathMap = start.getPathMap(c => costs[c]);
|
|
145
|
+
|
|
146
|
+
const pathToCentre = pathMap.get(50, 50);
|
|
147
|
+
const pathToCorner = pathMap.get(99, 99);
|
|
148
|
+
```
|
|
149
|
+
|
|
150
|
+
## The storage layer
|
|
151
|
+
|
|
152
|
+
`Grid` itself is an interface for reading and writing 2D data. `GridBase` - a subclass of `Grid` - maintains the actual storage of such data.
|
|
153
|
+
|
|
154
|
+
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)`.
|
|
155
|
+
|
|
156
|
+
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
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { Pipe2D } from "@xtia/pipe2d";
|
|
2
|
-
import {
|
|
2
|
+
import { GetXYFunc, Source2D } from "./utils.js";
|
|
3
3
|
type ChangeEvent<T> = {
|
|
4
4
|
changedCells: Set<Cell<T>>;
|
|
5
5
|
};
|
|
@@ -54,41 +54,28 @@ export declare class Cell<T> {
|
|
|
54
54
|
export declare class Grid<T> {
|
|
55
55
|
readonly width: number;
|
|
56
56
|
readonly height: number;
|
|
57
|
-
private batch;
|
|
58
57
|
private parentGet;
|
|
59
58
|
private parentSet;
|
|
59
|
+
private batch;
|
|
60
60
|
readonly cells: Pipe2D<Cell<T>>;
|
|
61
|
-
protected constructor(width: number, height: number,
|
|
62
|
-
private writeMasks;
|
|
61
|
+
protected constructor(width: number, height: number, parentGet: GetXYFunc<T>, parentSet: (x: number, y: number, value: T) => void, batch: (cb: () => void) => void);
|
|
63
62
|
pipe: Pipe2D<T>;
|
|
64
|
-
private isWritable;
|
|
65
63
|
batchUpdate(fn: () => void): void;
|
|
66
64
|
get(x: number, y: number): T;
|
|
67
65
|
set(x: number, y: number, value: T): void;
|
|
68
66
|
trySet(x: number, y: number, value: T): boolean;
|
|
69
|
-
fill(value: T): void;
|
|
70
|
-
paste(x: number, y: number, source: Source2D<T>): void;
|
|
71
|
-
applyWriteMask(mask: Source2D<boolean>): () => void;
|
|
67
|
+
fill(value: T, mask?: Source2D<boolean> | GetXYFunc<boolean>): void;
|
|
68
|
+
paste(x: number, y: number, source: Source2D<T>, mask?: Source2D<boolean> | GetXYFunc<boolean>): void;
|
|
72
69
|
[Symbol.iterator](): IterableIterator<{
|
|
73
70
|
x: number;
|
|
74
71
|
y: number;
|
|
75
72
|
value: T;
|
|
76
73
|
}>;
|
|
77
74
|
forEach(callback: (value: T, x: number, y: number) => void): void;
|
|
78
|
-
getVisibilityMap(originX: number, originY: number, maxDistance: number, isWall: (value: T, x: number, y: number) => boolean, options?: {
|
|
79
|
-
startAngle?: Angle;
|
|
80
|
-
endAngle?: Angle;
|
|
81
|
-
includeWalls?: boolean;
|
|
82
|
-
angleStep?: Angle;
|
|
83
|
-
}): Pipe2D<boolean>;
|
|
84
|
-
getFloodMap(startX: number, startY: number, predicate: (value: T, x: number, y: number, initialValue: T) => boolean, options?: {
|
|
85
|
-
includeDiagonals?: boolean;
|
|
86
|
-
maxDistance?: number;
|
|
87
|
-
}): Pipe2D<boolean>;
|
|
88
75
|
combine<U, R>(aux: Source2D<U>, cb: (source: T, aux: U) => R): GridBase<R>;
|
|
89
76
|
map<U>(read: (initial: T, x: number, y: number) => U, write: (local: U, x: number, y: number) => T): Grid<U>;
|
|
90
77
|
region(x: number, y: number, width: number, height: number): Grid<T>;
|
|
91
|
-
static init<T>(width: number, height: number, initCell: () => T): GridBase<T>;
|
|
78
|
+
static init<T>(width: number, height: number, initCell: (x: number, y: number) => T): GridBase<T>;
|
|
92
79
|
static from<T>(source: Source2D<T>): GridBase<T>;
|
|
93
80
|
static solid<T>(width: number, height: number, fillValue: T): GridBase<T>;
|
|
94
81
|
}
|
|
@@ -99,7 +86,6 @@ export declare class GridBase<T> extends Grid<T> {
|
|
|
99
86
|
private batchState;
|
|
100
87
|
batchUpdate(fn: () => void): void;
|
|
101
88
|
constructor(source: Source2D<T>);
|
|
102
|
-
constructor(width: number, height: number, cellInit: GetXYFunc<T>);
|
|
103
89
|
on<K extends keyof EventMap<T>>(eventName: K, handler: (data: EventMap<T>[K]) => void): () => void;
|
|
104
90
|
private xyToIndex;
|
|
105
91
|
private _set;
|
package/lib/grid.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { Pipe2D } from "@xtia/pipe2d";
|
|
2
|
-
import {
|
|
2
|
+
import { OrderedQueue } from "./utils.js";
|
|
3
3
|
export var TraversalType;
|
|
4
4
|
(function (TraversalType) {
|
|
5
5
|
TraversalType[TraversalType["cardinal"] = 0] = "cardinal";
|
|
@@ -177,6 +177,8 @@ export class Cell {
|
|
|
177
177
|
}
|
|
178
178
|
createVisibilityMap(isWall, maxDistance = Infinity) {
|
|
179
179
|
return new Pipe2D(this.gridView.width, this.gridView.height, (x, y) => {
|
|
180
|
+
if (isWall(this))
|
|
181
|
+
return Visibility.hidden;
|
|
180
182
|
const distance = getDistance(this, { x, y });
|
|
181
183
|
if (distance > maxDistance) {
|
|
182
184
|
return Visibility.hidden;
|
|
@@ -200,20 +202,16 @@ function getDistance(c1, c2) {
|
|
|
200
202
|
return Math.sqrt(Math.pow(c2.x - c1.x, 2) + Math.pow(c2.y - c1.y, 2));
|
|
201
203
|
}
|
|
202
204
|
export class Grid {
|
|
203
|
-
constructor(width, height,
|
|
205
|
+
constructor(width, height, parentGet, parentSet, batch) {
|
|
204
206
|
this.width = width;
|
|
205
207
|
this.height = height;
|
|
208
|
+
this.parentGet = parentGet;
|
|
209
|
+
this.parentSet = parentSet;
|
|
206
210
|
this.batch = batch;
|
|
207
211
|
this.cells = new Pipe2D(this.width, this.height, (x, y) => {
|
|
208
212
|
return new Cell(x, y, this);
|
|
209
213
|
}).strict().withCache();
|
|
210
|
-
this.writeMasks = [];
|
|
211
214
|
this.pipe = new Pipe2D(this);
|
|
212
|
-
this.parentGet = get;
|
|
213
|
-
this.parentSet = set;
|
|
214
|
-
}
|
|
215
|
-
isWritable(x, y) {
|
|
216
|
-
return !this.writeMasks.some(mask => !mask.mask.get(x, y));
|
|
217
215
|
}
|
|
218
216
|
batchUpdate(fn) {
|
|
219
217
|
this.batch(fn);
|
|
@@ -228,8 +226,7 @@ export class Grid {
|
|
|
228
226
|
if (x < 0 || x >= this.width || y < 0 || y >= this.height) {
|
|
229
227
|
throw new RangeError("Coordinates out of bounds");
|
|
230
228
|
}
|
|
231
|
-
|
|
232
|
-
this.parentSet(x, y, value);
|
|
229
|
+
this.parentSet(x, y, value);
|
|
233
230
|
}
|
|
234
231
|
trySet(x, y, value) {
|
|
235
232
|
if (x < 0 || x >= this.width || y < 0 || y >= this.height)
|
|
@@ -237,39 +234,41 @@ export class Grid {
|
|
|
237
234
|
this.set(x, y, value);
|
|
238
235
|
return true;
|
|
239
236
|
}
|
|
240
|
-
fill(value) {
|
|
237
|
+
fill(value, mask) {
|
|
238
|
+
const maskSource = typeof mask == "function" ? {
|
|
239
|
+
width: this.width,
|
|
240
|
+
height: this.height,
|
|
241
|
+
get: mask
|
|
242
|
+
} : mask;
|
|
241
243
|
this.batchUpdate(() => {
|
|
242
244
|
for (let y = 0; y < this.height; y++) {
|
|
243
245
|
for (let x = 0; x < this.width; x++) {
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
this.trySet(
|
|
246
|
+
if (maskSource && !maskSource.get(x, y))
|
|
247
|
+
continue;
|
|
248
|
+
this.trySet(x, y, value);
|
|
247
249
|
}
|
|
248
250
|
}
|
|
249
251
|
});
|
|
250
252
|
}
|
|
251
|
-
paste(x, y, source) {
|
|
253
|
+
paste(x, y, source, mask) {
|
|
254
|
+
const maskSource = typeof mask == "function" ? {
|
|
255
|
+
width: this.width,
|
|
256
|
+
height: this.height,
|
|
257
|
+
get: mask
|
|
258
|
+
} : mask;
|
|
252
259
|
const cachedSource = new GridBase(source);
|
|
253
260
|
this.batchUpdate(() => {
|
|
254
261
|
for (let oy = 0; oy < source.height; oy++) {
|
|
255
262
|
for (let ox = 0; ox < source.width; ox++) {
|
|
256
263
|
const tx = ox + x;
|
|
257
264
|
const ty = oy + y;
|
|
265
|
+
if (maskSource && !maskSource.get(tx, ty))
|
|
266
|
+
continue;
|
|
258
267
|
this.trySet(tx, ty, cachedSource.get(ox, oy));
|
|
259
268
|
}
|
|
260
269
|
}
|
|
261
270
|
});
|
|
262
271
|
}
|
|
263
|
-
applyWriteMask(mask) {
|
|
264
|
-
const unique = { mask };
|
|
265
|
-
this.writeMasks.push(unique);
|
|
266
|
-
return () => {
|
|
267
|
-
const idx = this.writeMasks.indexOf(unique);
|
|
268
|
-
if (idx == -1)
|
|
269
|
-
throw new Error("Mask was not present");
|
|
270
|
-
this.writeMasks.splice(idx, 1);
|
|
271
|
-
};
|
|
272
|
-
}
|
|
273
272
|
*[Symbol.iterator]() {
|
|
274
273
|
for (let y = 0; y < this.height; y++) {
|
|
275
274
|
for (let x = 0; x < this.width; x++) {
|
|
@@ -284,91 +283,15 @@ export class Grid {
|
|
|
284
283
|
}
|
|
285
284
|
}
|
|
286
285
|
}
|
|
287
|
-
getVisibilityMap(originX, originY, maxDistance, isWall, options) {
|
|
288
|
-
const { startAngle = { asTurns: 0 }, endAngle = { asTurns: 1 }, includeWalls = false, angleStep = { asDegrees: 1 } } = options || {};
|
|
289
|
-
const visibilityGrid = new GridBase(this.width, this.height, () => false);
|
|
290
|
-
const startRad = angleToRadians(startAngle);
|
|
291
|
-
const endRad = angleToRadians(endAngle);
|
|
292
|
-
const stepRad = angleToRadians(angleStep);
|
|
293
|
-
const totalAngle = endRad > startRad
|
|
294
|
-
? endRad - startRad
|
|
295
|
-
: endRad + (2 * Math.PI) - startRad;
|
|
296
|
-
const effectiveStepRad = Math.max(0.001, Math.min(stepRad, totalAngle));
|
|
297
|
-
const steps = Math.ceil(totalAngle / effectiveStepRad);
|
|
298
|
-
for (let i = 0; i <= steps; i++) {
|
|
299
|
-
const angle = startRad + i * effectiveStepRad;
|
|
300
|
-
if (angle > endRad && i > 0)
|
|
301
|
-
break;
|
|
302
|
-
const endX = Math.round(originX + maxDistance * Math.cos(angle));
|
|
303
|
-
const endY = Math.round(originY + maxDistance * Math.sin(angle));
|
|
304
|
-
let x0 = originX;
|
|
305
|
-
let y0 = originY;
|
|
306
|
-
let x1 = endX;
|
|
307
|
-
let y1 = endY;
|
|
308
|
-
const dx = Math.abs(x1 - x0);
|
|
309
|
-
const dy = Math.abs(y1 - y0);
|
|
310
|
-
const sx = x0 < x1 ? 1 : -1;
|
|
311
|
-
const sy = y0 < y1 ? 1 : -1;
|
|
312
|
-
let err = dx - dy;
|
|
313
|
-
while (true) {
|
|
314
|
-
if (x0 < 0 || x0 >= this.width || y0 < 0 || y0 >= this.height) {
|
|
315
|
-
break;
|
|
316
|
-
}
|
|
317
|
-
const isWallCell = isWall(this.get(x0, y0), x0, y0);
|
|
318
|
-
if (isWallCell) {
|
|
319
|
-
if (includeWalls) {
|
|
320
|
-
visibilityGrid.set(x0, y0, true);
|
|
321
|
-
}
|
|
322
|
-
break;
|
|
323
|
-
}
|
|
324
|
-
else {
|
|
325
|
-
visibilityGrid.set(x0, y0, true);
|
|
326
|
-
}
|
|
327
|
-
if (x0 === x1 && y0 === y1)
|
|
328
|
-
break;
|
|
329
|
-
const e2 = 2 * err;
|
|
330
|
-
if (e2 > -dy) {
|
|
331
|
-
err -= dy;
|
|
332
|
-
x0 += sx;
|
|
333
|
-
}
|
|
334
|
-
if (e2 < dx) {
|
|
335
|
-
err += dx;
|
|
336
|
-
y0 += sy;
|
|
337
|
-
}
|
|
338
|
-
}
|
|
339
|
-
}
|
|
340
|
-
return visibilityGrid.pipe;
|
|
341
|
-
}
|
|
342
|
-
getFloodMap(startX, startY, predicate, options) {
|
|
343
|
-
const { includeDiagonals = false, maxDistance = Infinity } = options || {};
|
|
344
|
-
const result = new GridBase(this.width, this.height, () => false);
|
|
345
|
-
const startValue = this.get(startX, startY);
|
|
346
|
-
if (!predicate(startValue, startX, startY, startValue)) {
|
|
347
|
-
return result.pipe;
|
|
348
|
-
}
|
|
349
|
-
const queue = [[startX, startY, 0]];
|
|
350
|
-
result.set(startX, startY, true);
|
|
351
|
-
while (queue.length > 0) {
|
|
352
|
-
const [x, y, distance] = queue.shift();
|
|
353
|
-
if (distance >= maxDistance)
|
|
354
|
-
continue;
|
|
355
|
-
this.cells.get(x, y).getNeighbours(includeDiagonals)
|
|
356
|
-
.forEach(neighbour => {
|
|
357
|
-
if (result.get(neighbour.x, neighbour.y))
|
|
358
|
-
return;
|
|
359
|
-
if (predicate(neighbour.value, neighbour.x, neighbour.y, startValue)) {
|
|
360
|
-
result.set(neighbour.x, neighbour.y, true);
|
|
361
|
-
queue.push([neighbour.x, neighbour.y, distance + 1]);
|
|
362
|
-
}
|
|
363
|
-
});
|
|
364
|
-
}
|
|
365
|
-
return result.pipe;
|
|
366
|
-
}
|
|
367
286
|
combine(aux, cb) {
|
|
368
287
|
const width = Math.min(this.width, aux.width);
|
|
369
288
|
const height = Math.min(this.height, aux.height);
|
|
370
|
-
return new GridBase(
|
|
371
|
-
|
|
289
|
+
return new GridBase({
|
|
290
|
+
width,
|
|
291
|
+
height,
|
|
292
|
+
get: (x, y) => {
|
|
293
|
+
return cb(this.get(x, y), aux.get(x, y));
|
|
294
|
+
}
|
|
372
295
|
});
|
|
373
296
|
}
|
|
374
297
|
map(read, write) {
|
|
@@ -384,7 +307,11 @@ export class Grid {
|
|
|
384
307
|
}, fn => this.batch(fn));
|
|
385
308
|
}
|
|
386
309
|
static init(width, height, initCell) {
|
|
387
|
-
return new GridBase(
|
|
310
|
+
return new GridBase({
|
|
311
|
+
width,
|
|
312
|
+
height,
|
|
313
|
+
get: initCell
|
|
314
|
+
});
|
|
388
315
|
}
|
|
389
316
|
static from(source) {
|
|
390
317
|
return new GridBase(source);
|
|
@@ -416,22 +343,14 @@ export class GridBase extends Grid {
|
|
|
416
343
|
this.triggerChange(changed);
|
|
417
344
|
}
|
|
418
345
|
}
|
|
419
|
-
constructor(
|
|
420
|
-
const source = typeof widthOrSource == "object"
|
|
421
|
-
? widthOrSource
|
|
422
|
-
: {
|
|
423
|
-
width: widthOrSource,
|
|
424
|
-
height: height,
|
|
425
|
-
get: cellInit
|
|
426
|
-
};
|
|
346
|
+
constructor(source) {
|
|
427
347
|
super(source.width, source.height, (x, y) => this.data[this.xyToIndex(x, y)], (x, y, value) => this._set(x, y, value), fn => this.batchUpdate(fn));
|
|
428
348
|
this.eventHandlers = {};
|
|
429
349
|
this.batchState = null;
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
});
|
|
350
|
+
const sourcePipe = source instanceof Pipe2D
|
|
351
|
+
? source
|
|
352
|
+
: new Pipe2D(source);
|
|
353
|
+
this.data = sourcePipe.toFlatArrayXY();
|
|
435
354
|
}
|
|
436
355
|
on(eventName, handler) {
|
|
437
356
|
const unique = { fn: handler };
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
|
-
"version": "0.0.
|
|
3
|
-
"description": "2D store and utilities",
|
|
2
|
+
"version": "0.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": {
|
|
6
6
|
".": {
|
|
@@ -11,16 +11,14 @@
|
|
|
11
11
|
},
|
|
12
12
|
"scripts": {
|
|
13
13
|
"build": "npx tsc",
|
|
14
|
-
"test": "npx
|
|
14
|
+
"test": "npx vitest run"
|
|
15
15
|
},
|
|
16
16
|
"files": [
|
|
17
17
|
"lib/"
|
|
18
18
|
],
|
|
19
19
|
"devDependencies": {
|
|
20
|
-
"
|
|
21
|
-
"
|
|
22
|
-
"jest": "^30.4.2",
|
|
23
|
-
"typescript": "^7.0.2"
|
|
20
|
+
"typescript": "^7.0.2",
|
|
21
|
+
"vitest": "^4.1.10"
|
|
24
22
|
},
|
|
25
23
|
"dependencies": {
|
|
26
24
|
"@xtia/pipe2d": "^2.1.0"
|