@xtia/grid 0.0.1

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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Aleta Lovelace
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/lib/grid.d.ts ADDED
@@ -0,0 +1,108 @@
1
+ import { Pipe2D } from "@xtia/pipe2d";
2
+ import { Angle, GetXYFunc, Source2D } from "./utils.js";
3
+ type ChangeEvent<T> = {
4
+ changedCells: Set<Cell<T>>;
5
+ };
6
+ type EventMap<T> = {
7
+ change: ChangeEvent<T>;
8
+ };
9
+ type PathFindOptions<T> = {
10
+ maxCost?: number;
11
+ allowDiagonal?: boolean;
12
+ onVisit?: (info: {
13
+ cell: Cell<T>;
14
+ cost: number;
15
+ }) => void;
16
+ shortcutMap?: Source2D<Cell<T>[] | undefined>;
17
+ };
18
+ type CostMapOptions<T> = PathFindOptions<T> & {
19
+ stopAt?: [number, number] | Cell<T>;
20
+ };
21
+ export declare enum TraversalType {
22
+ cardinal = 0,
23
+ diagonal = 1,
24
+ shortcut = 2
25
+ }
26
+ type CostFunc<T> = (cell: Cell<T>, context: {
27
+ traversalType: TraversalType;
28
+ from: Cell<T>;
29
+ }) => number;
30
+ export declare enum Visibility {
31
+ visible = 0,
32
+ hidden = 1,
33
+ wall = 2
34
+ }
35
+ export declare class Cell<T> {
36
+ readonly x: number;
37
+ readonly y: number;
38
+ private gridView;
39
+ constructor(x: number, y: number, gridView: Grid<T>);
40
+ get value(): T;
41
+ set value(v: T);
42
+ getNeighbours(includeDiagonals?: boolean): Cell<T>[];
43
+ look(xDelta: number, yDelta: number): Cell<T> | undefined;
44
+ findPath(target: Cell<T> | [number, number], getCost: CostFunc<T>, options?: PathFindOptions<T>): Cell<T>[] | null;
45
+ getPathMap(getCost: CostFunc<T>, options?: PathFindOptions<T>): Pipe2D<Cell<T>[] | null>;
46
+ getCostMap(getCost: CostFunc<T>, options?: CostMapOptions<T>): Pipe2D<number>;
47
+ private calculateCosts;
48
+ getLineTo(target: {
49
+ x: number;
50
+ y: number;
51
+ } | [number, number]): IterableIterator<Cell<T>>;
52
+ createVisibilityMap(isWall: (cell: Cell<T>) => boolean, maxDistance?: number): Pipe2D<Visibility>;
53
+ }
54
+ export declare class Grid<T> {
55
+ readonly width: number;
56
+ readonly height: number;
57
+ private batch;
58
+ private parentGet;
59
+ private parentSet;
60
+ readonly cells: Pipe2D<Cell<T>>;
61
+ protected constructor(width: number, height: number, get: GetXYFunc<T>, set: (x: number, y: number, value: T) => void, batch: (cb: () => void) => void);
62
+ private writeMasks;
63
+ pipe: Pipe2D<T>;
64
+ private isWritable;
65
+ batchUpdate(fn: () => void): void;
66
+ get(x: number, y: number): T;
67
+ set(x: number, y: number, value: T): void;
68
+ 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;
72
+ [Symbol.iterator](): IterableIterator<{
73
+ x: number;
74
+ y: number;
75
+ value: T;
76
+ }>;
77
+ 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
+ combine<U, R>(aux: Source2D<U>, cb: (source: T, aux: U) => R): GridBase<R>;
89
+ map<U>(read: (initial: T, x: number, y: number) => U, write: (local: U, x: number, y: number) => T): Grid<U>;
90
+ region(x: number, y: number, width: number, height: number): Grid<T>;
91
+ static init<T>(width: number, height: number, initCell: () => T): GridBase<T>;
92
+ static from<T>(source: Source2D<T>): GridBase<T>;
93
+ static solid<T>(width: number, height: number, fillValue: T): GridBase<T>;
94
+ }
95
+ export declare class GridBase<T> extends Grid<T> {
96
+ private data;
97
+ private eventHandlers;
98
+ private triggerEvent;
99
+ private batchState;
100
+ batchUpdate(fn: () => void): void;
101
+ constructor(source: Source2D<T>);
102
+ constructor(width: number, height: number, cellInit: GetXYFunc<T>);
103
+ on<K extends keyof EventMap<T>>(eventName: K, handler: (data: EventMap<T>[K]) => void): () => void;
104
+ private xyToIndex;
105
+ private _set;
106
+ private triggerChange;
107
+ }
108
+ export {};
package/lib/grid.js ADDED
@@ -0,0 +1,470 @@
1
+ import { Pipe2D } from "@xtia/pipe2d";
2
+ import { angleToRadians, OrderedQueue } from "./utils.js";
3
+ export var TraversalType;
4
+ (function (TraversalType) {
5
+ TraversalType[TraversalType["cardinal"] = 0] = "cardinal";
6
+ TraversalType[TraversalType["diagonal"] = 1] = "diagonal";
7
+ TraversalType[TraversalType["shortcut"] = 2] = "shortcut";
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
+ export class Cell {
16
+ constructor(x, y, gridView) {
17
+ this.x = x;
18
+ this.y = y;
19
+ this.gridView = gridView;
20
+ }
21
+ get value() {
22
+ return this.gridView.get(this.x, this.y);
23
+ }
24
+ set value(v) {
25
+ this.gridView.set(this.x, this.y, v);
26
+ }
27
+ getNeighbours(includeDiagonals = false) {
28
+ const offsets = includeDiagonals
29
+ ? [[-1, -1], [-1, 0], [-1, 1], [0, -1], [0, 1], [1, -1], [1, 0], [1, 1]]
30
+ : [[-1, 0], [1, 0], [0, -1], [0, 1]];
31
+ return offsets
32
+ .map(([ox, oy]) => [ox + this.x, oy + this.y])
33
+ .filter(([tx, ty]) => tx >= 0 && tx < this.gridView.width && ty >= 0 && ty < this.gridView.height)
34
+ .map(([tx, ty]) => this.gridView.cells.get(tx, ty));
35
+ }
36
+ look(xDelta, yDelta) {
37
+ const [x, y] = [this.x + xDelta, this.y + yDelta];
38
+ if (x < 0 || x >= this.gridView.width || y < 0 || y >= this.gridView.height)
39
+ return undefined;
40
+ return this.gridView.cells.get(xDelta + this.x, yDelta + this.y);
41
+ }
42
+ findPath(target, getCost, options = {}) {
43
+ const targetXY = Array.isArray(target)
44
+ ? target
45
+ : [target.x, target.y];
46
+ const pathMap = this.calculateCosts(getCost, {
47
+ stopAt: targetXY,
48
+ allowDiagonal: options.allowDiagonal,
49
+ maxCost: options.maxCost,
50
+ }, true);
51
+ return pathMap === null || pathMap === void 0 ? void 0 : pathMap.get(...targetXY);
52
+ }
53
+ getPathMap(getCost, options = {}) {
54
+ return this.calculateCosts(getCost, options, true);
55
+ }
56
+ getCostMap(getCost, options = {}) {
57
+ return this.calculateCosts(getCost, options);
58
+ }
59
+ calculateCosts(getCost, options, asPathMap = false) {
60
+ var _a, _b;
61
+ var _c;
62
+ const visited = new Set();
63
+ const costs = new Map();
64
+ const maxCost = (_c = options.maxCost) !== null && _c !== void 0 ? _c : Infinity;
65
+ const pathInfo = asPathMap
66
+ ? new Map()
67
+ : null;
68
+ costs.set(this, 0);
69
+ const queue = new OrderedQueue(cell => { var _a; return (_a = costs.get(cell)) !== null && _a !== void 0 ? _a : Infinity; }, this);
70
+ const targetCell = Array.isArray(options.stopAt)
71
+ ? this.gridView.cells.get(options.stopAt[0], options.stopAt[1])
72
+ : options.stopAt;
73
+ if (targetCell && targetCell.gridView !== this.gridView) {
74
+ throw new Error("Target cell belongs to a different Grid");
75
+ }
76
+ while (queue.length > 0) {
77
+ const current = queue.take();
78
+ (_a = options.onVisit) === null || _a === void 0 ? void 0 : _a.call(options, { cell: current, cost: costs.get(current) });
79
+ if (visited.has(current))
80
+ continue;
81
+ visited.add(current);
82
+ if (targetCell && current === targetCell) {
83
+ break;
84
+ }
85
+ const currentCost = costs.get(current);
86
+ const neighbours = current.getNeighbours(options.allowDiagonal).map(cell => ({
87
+ traversalType: cell.x == current.x && cell.y == current.y ? TraversalType.cardinal : TraversalType.diagonal,
88
+ cell
89
+ }));
90
+ if (options.shortcutMap) {
91
+ const shortcuts = (_b = options.shortcutMap.get(current.x, current.y)) === null || _b === void 0 ? void 0 : _b.map(cell => ({
92
+ traversalType: TraversalType.shortcut,
93
+ cell
94
+ }));
95
+ if (shortcuts)
96
+ neighbours.push(...shortcuts);
97
+ }
98
+ neighbours.forEach(n => {
99
+ var _a;
100
+ if (visited.has(n.cell))
101
+ return;
102
+ const moveCost = getCost(n.cell, {
103
+ traversalType: n.traversalType,
104
+ from: current,
105
+ });
106
+ if (moveCost === Infinity)
107
+ return;
108
+ const newCost = currentCost + moveCost;
109
+ const oldCost = (_a = costs.get(n.cell)) !== null && _a !== void 0 ? _a : Infinity;
110
+ if (newCost < oldCost && newCost <= maxCost) {
111
+ costs.set(n.cell, newCost);
112
+ pathInfo === null || pathInfo === void 0 ? void 0 : pathInfo.set(n.cell, current);
113
+ queue.add(n.cell);
114
+ }
115
+ });
116
+ }
117
+ if (pathInfo) {
118
+ const cache = new Map();
119
+ return this.gridView.cells.map(targetCell => {
120
+ if (targetCell === this)
121
+ return [];
122
+ if (cache.has(targetCell))
123
+ return cache.get(targetCell);
124
+ if (!pathInfo.has(targetCell))
125
+ return null;
126
+ let current = targetCell;
127
+ let path = [];
128
+ while (current !== this) {
129
+ if (cache.has(current)) {
130
+ path.unshift(...cache.get(current));
131
+ cache.set(targetCell, path);
132
+ return path;
133
+ }
134
+ path.unshift(current);
135
+ current = pathInfo.get(current);
136
+ }
137
+ cache.set(targetCell, path);
138
+ return path;
139
+ });
140
+ }
141
+ return this.gridView.cells.map((c => { var _a; return (_a = costs.get(c)) !== null && _a !== void 0 ? _a : Infinity; }))
142
+ .strict();
143
+ }
144
+ *getLineTo(target) {
145
+ const [targetX, targetY] = Array.isArray(target)
146
+ ? target
147
+ : [target.x, target.y];
148
+ if (this.x === targetX && this.y === targetY) {
149
+ yield this;
150
+ return;
151
+ }
152
+ const dx = Math.abs(targetX - this.x);
153
+ const dy = Math.abs(targetY - this.y);
154
+ const sx = this.x < targetX ? 1 : -1;
155
+ const sy = this.y < targetY ? 1 : -1;
156
+ let err = dx - dy;
157
+ let x = this.x;
158
+ let y = this.y;
159
+ while (true) {
160
+ const cell = this.gridView.cells.get(x, y);
161
+ if (cell) {
162
+ yield cell;
163
+ }
164
+ if (x === targetX && y === targetY) {
165
+ break;
166
+ }
167
+ const e2 = 2 * err;
168
+ if (e2 > -dy) {
169
+ err -= dy;
170
+ x += sx;
171
+ }
172
+ if (e2 < dx) {
173
+ err += dx;
174
+ y += sy;
175
+ }
176
+ }
177
+ }
178
+ createVisibilityMap(isWall, maxDistance = Infinity) {
179
+ return new Pipe2D(this.gridView.width, this.gridView.height, (x, y) => {
180
+ const distance = getDistance(this, { x, y });
181
+ if (distance > maxDistance) {
182
+ return Visibility.hidden;
183
+ }
184
+ for (const cell of this.getLineTo({ x, y })) {
185
+ if (isWall(cell)) {
186
+ if (cell.x === this.x && cell.y === this.y) {
187
+ continue;
188
+ }
189
+ if (cell.x === x && cell.y === y) {
190
+ return Visibility.wall;
191
+ }
192
+ return Visibility.hidden;
193
+ }
194
+ }
195
+ return Visibility.visible;
196
+ });
197
+ }
198
+ }
199
+ function getDistance(c1, c2) {
200
+ return Math.sqrt(Math.pow(c2.x - c1.x, 2) + Math.pow(c2.y - c1.y, 2));
201
+ }
202
+ export class Grid {
203
+ constructor(width, height, get, set, batch) {
204
+ this.width = width;
205
+ this.height = height;
206
+ this.batch = batch;
207
+ this.cells = new Pipe2D(this.width, this.height, (x, y) => {
208
+ return new Cell(x, y, this);
209
+ }).strict().withCache();
210
+ this.writeMasks = [];
211
+ 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
+ }
218
+ batchUpdate(fn) {
219
+ this.batch(fn);
220
+ }
221
+ get(x, y) {
222
+ if (x < 0 || x >= this.width || y < 0 || y >= this.height) {
223
+ throw new RangeError("Coordinates out of bounds");
224
+ }
225
+ return this.parentGet(x, y);
226
+ }
227
+ set(x, y, value) {
228
+ if (x < 0 || x >= this.width || y < 0 || y >= this.height) {
229
+ throw new RangeError("Coordinates out of bounds");
230
+ }
231
+ if (this.isWritable(x, y))
232
+ this.parentSet(x, y, value);
233
+ }
234
+ trySet(x, y, value) {
235
+ if (x < 0 || x >= this.width || y < 0 || y >= this.height)
236
+ return false;
237
+ this.set(x, y, value);
238
+ return true;
239
+ }
240
+ fill(value) {
241
+ this.batchUpdate(() => {
242
+ for (let y = 0; y < this.height; y++) {
243
+ for (let x = 0; x < this.width; x++) {
244
+ const tx = x;
245
+ const ty = y;
246
+ this.trySet(tx, ty, value);
247
+ }
248
+ }
249
+ });
250
+ }
251
+ paste(x, y, source) {
252
+ const cachedSource = new GridBase(source);
253
+ this.batchUpdate(() => {
254
+ for (let oy = 0; oy < source.height; oy++) {
255
+ for (let ox = 0; ox < source.width; ox++) {
256
+ const tx = ox + x;
257
+ const ty = oy + y;
258
+ this.trySet(tx, ty, cachedSource.get(ox, oy));
259
+ }
260
+ }
261
+ });
262
+ }
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
+ *[Symbol.iterator]() {
274
+ for (let y = 0; y < this.height; y++) {
275
+ for (let x = 0; x < this.width; x++) {
276
+ yield { x, y, value: this.get(x, y) };
277
+ }
278
+ }
279
+ }
280
+ forEach(callback) {
281
+ for (let y = 0; y < this.height; y++) {
282
+ for (let x = 0; x < this.width; x++) {
283
+ callback(this.get(x, y), x, y);
284
+ }
285
+ }
286
+ }
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
+ combine(aux, cb) {
368
+ const width = Math.min(this.width, aux.width);
369
+ const height = Math.min(this.height, aux.height);
370
+ return new GridBase(width, height, (x, y) => {
371
+ return cb(this.get(x, y), aux.get(x, y));
372
+ });
373
+ }
374
+ map(read, write) {
375
+ 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));
376
+ }
377
+ region(x, y, width, height) {
378
+ return new Grid(width, height, (tx, ty) => {
379
+ return this.parentGet(tx + x, ty + y);
380
+ }, (tx, ty, value) => {
381
+ if (tx < 0 || ty < 0 || tx >= width || ty >= height)
382
+ throw new Error("Out of bounds");
383
+ this.parentSet(tx + x, ty + y, value);
384
+ }, fn => this.batch(fn));
385
+ }
386
+ static init(width, height, initCell) {
387
+ return new GridBase(width, height, initCell);
388
+ }
389
+ static from(source) {
390
+ return new GridBase(source);
391
+ }
392
+ static solid(width, height, fillValue) {
393
+ return new GridBase(Pipe2D.solid(fillValue, width, height));
394
+ }
395
+ }
396
+ export class GridBase extends Grid {
397
+ triggerEvent(name, data) {
398
+ var _a;
399
+ (_a = this.eventHandlers[name]) === null || _a === void 0 ? void 0 : _a.forEach(obj => obj.fn(data));
400
+ }
401
+ batchUpdate(fn) {
402
+ if (this.batchState) {
403
+ this.batchState.level++;
404
+ }
405
+ else {
406
+ this.batchState = {
407
+ level: 1,
408
+ changedCells: new Set()
409
+ };
410
+ }
411
+ fn();
412
+ this.batchState.level--;
413
+ if (this.batchState.level == 0) {
414
+ const changed = this.batchState.changedCells;
415
+ this.batchState = null;
416
+ this.triggerChange(changed);
417
+ }
418
+ }
419
+ constructor(widthOrSource, height, cellInit) {
420
+ const source = typeof widthOrSource == "object"
421
+ ? widthOrSource
422
+ : {
423
+ width: widthOrSource,
424
+ height: height,
425
+ get: cellInit
426
+ };
427
+ 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
+ this.eventHandlers = {};
429
+ this.batchState = null;
430
+ this.data = Array.from({ length: source.width * source.height }, (_, i) => {
431
+ const x = i % source.width;
432
+ const y = Math.floor(i / source.width);
433
+ return source.get(x, y);
434
+ });
435
+ }
436
+ on(eventName, handler) {
437
+ const unique = { fn: handler };
438
+ if (!this.eventHandlers[eventName])
439
+ this.eventHandlers[eventName] = [];
440
+ this.eventHandlers[eventName].push(unique);
441
+ return () => {
442
+ const idx = this.eventHandlers[eventName].indexOf(unique);
443
+ if (idx === -1)
444
+ throw new Error("Event handler was already retired");
445
+ this.eventHandlers[eventName].splice(idx, 1);
446
+ };
447
+ }
448
+ xyToIndex(x, y) {
449
+ if (x < 0 || x >= this.width || y < 0 || y >= this.height) {
450
+ throw new Error(`Coordinates out of bounds: (${x}, ${y})`);
451
+ }
452
+ return y * this.width + x;
453
+ }
454
+ _set(x, y, value) {
455
+ const idx = this.xyToIndex(x, y);
456
+ if (this.data[idx] === value)
457
+ return;
458
+ if (this.batchState) {
459
+ this.batchState.changedCells.add(this.cells.get(x, y));
460
+ return;
461
+ }
462
+ this.data[this.xyToIndex(x, y)] = value;
463
+ if (this.eventHandlers.change) {
464
+ this.triggerChange(new Set([this.cells.get(x, y)]));
465
+ }
466
+ }
467
+ triggerChange(changed) {
468
+ this.triggerEvent("change", { changedCells: changed });
469
+ }
470
+ }
package/lib/index.d.ts ADDED
@@ -0,0 +1 @@
1
+ export { Grid, type GridBase, type Cell, TraversalType, Visibility } from "./grid.js";
package/lib/index.js ADDED
@@ -0,0 +1 @@
1
+ export { Grid, TraversalType, Visibility } from "./grid.js";
package/lib/utils.d.ts ADDED
@@ -0,0 +1,22 @@
1
+ export type GetXYFunc<T> = (x: number, y: number) => T;
2
+ export type Source2D<T> = {
3
+ width: number;
4
+ height: number;
5
+ get: GetXYFunc<T>;
6
+ };
7
+ export type Angle = {
8
+ asDegrees: number;
9
+ } | {
10
+ asRadians: number;
11
+ } | {
12
+ asTurns: number;
13
+ };
14
+ export declare function angleToRadians(angle: Angle): number;
15
+ export declare class OrderedQueue<T> {
16
+ private getCost;
17
+ private queue;
18
+ constructor(getCost: (value: T) => number, ...values: T[]);
19
+ add(value: T): void;
20
+ get length(): number;
21
+ take(): T;
22
+ }
package/lib/utils.js ADDED
@@ -0,0 +1,39 @@
1
+ export function angleToRadians(angle) {
2
+ if ("asRadians" in angle)
3
+ return angle.asRadians;
4
+ if ("asDegrees" in angle)
5
+ return angle.asDegrees * (180 / Math.PI);
6
+ if ("asTurns" in angle)
7
+ return angle.asTurns * Math.PI * 2;
8
+ throw new Error("Invalid angle");
9
+ }
10
+ export class OrderedQueue {
11
+ constructor(getCost, ...values) {
12
+ this.getCost = getCost;
13
+ this.queue = [];
14
+ values.forEach(v => this.add(v));
15
+ }
16
+ add(value) {
17
+ const priority = this.getCost(value);
18
+ const item = { value, priority };
19
+ let low = 0, high = this.queue.length;
20
+ while (low < high) {
21
+ const mid = (low + high) >> 1;
22
+ if (this.queue[mid].priority > priority) {
23
+ low = mid + 1;
24
+ }
25
+ else {
26
+ high = mid;
27
+ }
28
+ }
29
+ this.queue.splice(low, 0, item);
30
+ }
31
+ get length() {
32
+ return this.queue.length;
33
+ }
34
+ take() {
35
+ if (this.queue.length == 0)
36
+ throw new Error("Queue is empty");
37
+ return this.queue.pop().value;
38
+ }
39
+ }
package/package.json ADDED
@@ -0,0 +1,28 @@
1
+ {
2
+ "version": "0.0.1",
3
+ "description": "2D store and utilities",
4
+ "name": "@xtia/grid",
5
+ "exports": {
6
+ ".": {
7
+ "types": "./lib/index.d.ts",
8
+ "default": "./lib/index.js"
9
+ },
10
+ "./lib/*": null
11
+ },
12
+ "scripts": {
13
+ "build": "npx tsc",
14
+ "test": "npx jest"
15
+ },
16
+ "files": [
17
+ "lib/"
18
+ ],
19
+ "devDependencies": {
20
+ "@swc/jest": "^0.2.39",
21
+ "@types/jest": "^30.0.0",
22
+ "jest": "^30.4.2",
23
+ "typescript": "^7.0.2"
24
+ },
25
+ "dependencies": {
26
+ "@xtia/pipe2d": "^2.1.0"
27
+ }
28
+ }