The tilemap panel gains Fill and Pick: Fill floods the connected cells of the active layer that carry the same tile (edge neighbours only, bounded by the grid) as one undo step, Pick takes the tile of the clicked cell on the active layer, and holding Alt picks while any tool is active (#133)
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
1 parent
c2fd536aaf
commit
0b32d3cbf3
9 files changed
+350
-20
No files matched your search
@@ -0,0 +1,74 @@
|
||||
/**
|
||||
* The fill of a tilemap layer (#133): the cells reached from the one clicked through their four
|
||||
* edge neighbours that hold the same tile – empty included – and nothing outside the grid.
|
||||
*/
|
||||
import { describe, expect, it } from 'vitest';
|
||||
import type { TilemapLayer } from '../../src/core/model/Asset';
|
||||
import { cellKey, floodFill } from '../../src/core/model/tilemap';
|
||||
|
||||
/** A layer drawn as text: one row per line, `.` empty, any other letter the tile of that name. */
|
||||
function layerOf(rows: string[]): TilemapLayer {
|
||||
const cells: Record<string, string> = {};
|
||||
rows.forEach((row, y) =>
|
||||
[...row].forEach((c, x) => {
|
||||
if (c !== '.') cells[cellKey(x, y)] = c;
|
||||
}),
|
||||
);
|
||||
return { id: 'l', name: 'Layer', cells };
|
||||
}
|
||||
|
||||
/** The filled cells as text again, `*` where the fill landed – easier to read than a list. */
|
||||
function shown(rows: string[], filled: { x: number; y: number }[]): string[] {
|
||||
const out = rows.map((row) => [...row]);
|
||||
for (const { x, y } of filled) out[y]![x] = '*';
|
||||
return out.map((row) => row.join(''));
|
||||
}
|
||||
|
||||
const GRID = { columns: 5, rows: 4 };
|
||||
|
||||
describe('filling a tilemap layer', () => {
|
||||
it('takes the connected cells through edges, not through corners', () => {
|
||||
const rows = ['..w..', '.w.w.', 'w...w', '.w.w.'];
|
||||
const cells = floodFill(GRID, layerOf(rows), { x: 2, y: 2 }, 'g');
|
||||
expect(shown(rows, cells)).toEqual(['..w..', '.w*w.', 'w***w', '.w*w.']);
|
||||
});
|
||||
|
||||
it('fills a whole empty grid and stops at its edge', () => {
|
||||
const cells = floodFill(GRID, layerOf([]), { x: 0, y: 0 }, 'g');
|
||||
expect(cells).toHaveLength(20);
|
||||
expect(cells.every(({ x, y }) => x >= 0 && y >= 0 && x < 5 && y < 4)).toBe(true);
|
||||
});
|
||||
|
||||
it('replaces one tile with another and leaves the empty cells beside it', () => {
|
||||
const rows = ['aa...', 'a.bb.', 'aaab.', '.....'];
|
||||
const cells = floodFill(GRID, layerOf(rows), { x: 0, y: 0 }, 'b');
|
||||
expect(shown(rows, cells)).toEqual(['**...', '*.bb.', '***b.', '.....']);
|
||||
});
|
||||
|
||||
it('ignores cells outside the grid, even when they hold the same tile', () => {
|
||||
const layer = layerOf(['aaaaa']);
|
||||
layer.cells[cellKey(5, 0)] = 'a';
|
||||
layer.cells[cellKey(-1, 0)] = 'a';
|
||||
const cells = floodFill(GRID, layer, { x: 2, y: 0 }, 'g');
|
||||
expect(cells.map((c) => c.x).sort()).toEqual([0, 1, 2, 3, 4]);
|
||||
expect(floodFill(GRID, layer, { x: 5, y: 0 }, 'g')).toEqual([]);
|
||||
});
|
||||
|
||||
it('changes nothing when the cell already holds the tile', () => {
|
||||
expect(floodFill(GRID, layerOf(['aaaaa']), { x: 1, y: 0 }, 'a')).toEqual([]);
|
||||
expect(floodFill(GRID, layerOf([]), { x: 1, y: 1 }, '')).toEqual([]);
|
||||
});
|
||||
|
||||
it('can empty a region, too', () => {
|
||||
const rows = ['aa.a.'];
|
||||
const cells = floodFill(GRID, layerOf(rows), { x: 1, y: 0 }, '');
|
||||
expect(shown(rows, cells)).toEqual(['**.a.']);
|
||||
});
|
||||
|
||||
it('fills a large map in one go', () => {
|
||||
const props = { columns: 300, rows: 300 };
|
||||
const started = performance.now();
|
||||
expect(floodFill(props, layerOf([]), { x: 150, y: 150 }, 'g')).toHaveLength(90_000);
|
||||
expect(performance.now() - started).toBeLessThan(2000);
|
||||
});
|
||||
});
|
||||
Reference in new issue
Block a user