mirror of
https://github.com/Ark0N/Codeman.git
synced 2026-10-09 08:59:40 +02:00
feat(tiles): pure helpers for a grid whose empty cells can be anywhere
Owner: an empty cell need not be the last one ("the empty tab can also
be tab nr 4 or 3"). The helpers that let the grid hold cells instead of
a packed list, with no behaviour change on their own:
- fitTileCells: the cells after a shape change. The same shape keeps
every cell, holes included; a new shape keeps each tile at its row
and column when all fit (2x2 growing to 3x2: the four tiles stay
put), else the tiles pack in reading order.
- tileInDirection takes cells: focus never lands on an empty cell.
Left and right go along the row past a hole and never leave it; up
and down take the nearest row with a tile, the same column else the
nearest (lower on a tie). For a packed list this is exactly the old
rule, short last row included.
- tileCellInDirection: the adjacent cell a Move Tile chord moves into
or swaps with.
- sanitizeTileGridState reads the stored ids as cells (null for an
empty one) and returns them as `cells` beside the packed `ids`; a
dropped id becomes a hole, never a shift. The old packed format reads
as cells with no hole.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This commit is contained in:
+107
-25
@@ -1717,11 +1717,17 @@ function tileGridCapacity({ width, height }) {
|
|||||||
* must be 1 to 3 finite positive numbers. Anything that is not a v1 object
|
* must be 1 to 3 finite positive numbers. Anything that is not a v1 object
|
||||||
* (or its JSON) gives null.
|
* (or its JSON) gives null.
|
||||||
*
|
*
|
||||||
|
* The stored `ids` are the grid's CELLS in reading order, `null` for an empty
|
||||||
|
* one (a hole can be any cell). The old packed list (no nulls) reads as cells
|
||||||
|
* with no hole. `ids` comes back packed (the tiles in reading order, what
|
||||||
|
* every list consumer wants) and `cells` keeps the holes: a dropped id (gone,
|
||||||
|
* detached, a duplicate, past the cap) becomes `null` there, never a shift.
|
||||||
|
*
|
||||||
* @param {unknown} raw - the parsed value, or the stored JSON string
|
* @param {unknown} raw - the parsed value, or the stored JSON string
|
||||||
* @param {{has(id: string): boolean}|Iterable<string>} liveSessions - ids that exist now
|
* @param {{has(id: string): boolean}|Iterable<string>} liveSessions - ids that exist now
|
||||||
* @param {{has(id: string): boolean}} [detachedIds] - sessions popped out to their own window
|
* @param {{has(id: string): boolean}} [detachedIds] - sessions popped out to their own window
|
||||||
* @returns {{v: 1, open: boolean, ids: string[], focused: string|null, zoomed: string|null,
|
* @returns {{v: 1, open: boolean, ids: string[], cells: (string|null)[], focused: string|null,
|
||||||
* colFr: number[]|null, rowFr: number[]|null}|null}
|
* zoomed: string|null, colFr: number[]|null, rowFr: number[]|null}|null}
|
||||||
*/
|
*/
|
||||||
function sanitizeTileGridState(raw, liveSessions, detachedIds) {
|
function sanitizeTileGridState(raw, liveSessions, detachedIds) {
|
||||||
let value = raw;
|
let value = raw;
|
||||||
@@ -1731,12 +1737,14 @@ function sanitizeTileGridState(raw, liveSessions, detachedIds) {
|
|||||||
if (!value || typeof value !== 'object' || Array.isArray(value) || value.v !== 1) return null;
|
if (!value || typeof value !== 'object' || Array.isArray(value) || value.v !== 1) return null;
|
||||||
const live = liveSessions && typeof liveSessions.has === 'function' ? liveSessions : new Set(liveSessions || []);
|
const live = liveSessions && typeof liveSessions.has === 'function' ? liveSessions : new Set(liveSessions || []);
|
||||||
const ids = [];
|
const ids = [];
|
||||||
for (const id of Array.isArray(value.ids) ? value.ids : []) {
|
const cells = [];
|
||||||
if (typeof id !== 'string' || !id || ids.includes(id)) continue;
|
for (const id of (Array.isArray(value.ids) ? value.ids : []).slice(0, TILE_LAYOUT_MAX)) {
|
||||||
if (!live.has(id)) continue;
|
const keep =
|
||||||
if (detachedIds?.has?.(id)) continue;
|
typeof id === 'string' && id && !ids.includes(id) && live.has(id) && !detachedIds?.has?.(id) &&
|
||||||
ids.push(id);
|
ids.length < TILE_GRID_MAX;
|
||||||
if (ids.length === TILE_GRID_MAX) break;
|
if (keep) ids.push(id);
|
||||||
|
// A malformed entry (not a string, not null) is a hole too.
|
||||||
|
cells.push(keep ? id : null);
|
||||||
}
|
}
|
||||||
const fractions = (fr) => {
|
const fractions = (fr) => {
|
||||||
if (!Array.isArray(fr) || fr.length < 1 || fr.length > 3) return null;
|
if (!Array.isArray(fr) || fr.length < 1 || fr.length > 3) return null;
|
||||||
@@ -1746,6 +1754,7 @@ function sanitizeTileGridState(raw, liveSessions, detachedIds) {
|
|||||||
v: 1,
|
v: 1,
|
||||||
open: value.open === true && ids.length > 0,
|
open: value.open === true && ids.length > 0,
|
||||||
ids,
|
ids,
|
||||||
|
cells,
|
||||||
focused: ids.includes(value.focused) ? value.focused : (ids[0] ?? null),
|
focused: ids.includes(value.focused) ? value.focused : (ids[0] ?? null),
|
||||||
zoomed: ids.includes(value.zoomed) ? value.zoomed : null,
|
zoomed: ids.includes(value.zoomed) ? value.zoomed : null,
|
||||||
colFr: fractions(value.colFr),
|
colFr: fractions(value.colFr),
|
||||||
@@ -1855,31 +1864,102 @@ function tileNeighbor(ids, id) {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* The tile a directional focus chord moves to, in a row-major grid of `cols`
|
* The tile a directional focus chord moves to, in a row-major grid of `cols`
|
||||||
* columns. Left and right stay within the row; up and down move a whole row,
|
* columns whose empty cells are `null` (a hole can be any cell) or simply
|
||||||
* and moving down onto a short last row lands on its last tile. Null when
|
* missing at the end. Focus never lands on a hole. Left and right go along
|
||||||
* there is nothing in that direction.
|
* the row, past any hole, and never leave it. Up and down take the nearest
|
||||||
|
* row in that direction that has a tile: the tile in the same column, else
|
||||||
|
* the one in the nearest column (the lower column on a tie), so moving down
|
||||||
|
* onto a short or holed last row lands on its nearest tile. Null when there
|
||||||
|
* is no tile in that direction.
|
||||||
*
|
*
|
||||||
* @param {string[]} ids - the grid's tiles, in reading order
|
* @param {(string|null)[]} cells - the grid's cells in reading order (or its packed tiles)
|
||||||
* @param {string} focusedId - the tile the keyboard is in
|
* @param {string} focusedId - the tile the keyboard is in
|
||||||
* @param {'left'|'right'|'up'|'down'} direction
|
* @param {'left'|'right'|'up'|'down'} direction
|
||||||
* @param {number} cols - the layout's column count
|
* @param {number} cols - the layout's column count
|
||||||
* @returns {string|null}
|
* @returns {string|null}
|
||||||
*/
|
*/
|
||||||
function tileInDirection(ids, focusedId, direction, cols) {
|
function tileInDirection(cells, focusedId, direction, cols) {
|
||||||
const n = ids.length;
|
const i = focusedId ? cells.indexOf(focusedId) : -1;
|
||||||
const i = ids.indexOf(focusedId);
|
if (i === -1 || !(cols >= 1)) return null;
|
||||||
if (i === -1 || n === 0 || !(cols >= 1)) return null;
|
const rows = Math.ceil(cells.length / cols);
|
||||||
|
const row = Math.floor(i / cols);
|
||||||
const col = i % cols;
|
const col = i % cols;
|
||||||
let j = -1;
|
const at = (r, c) => cells[r * cols + c] || null;
|
||||||
if (direction === 'left') j = col > 0 ? i - 1 : -1;
|
if (direction === 'left' || direction === 'right') {
|
||||||
else if (direction === 'right') j = col < cols - 1 && i + 1 < n ? i + 1 : -1;
|
const step = direction === 'left' ? -1 : 1;
|
||||||
else if (direction === 'up') j = i - cols;
|
for (let c = col + step; c >= 0 && c < cols; c += step) {
|
||||||
else if (direction === 'down') {
|
if (at(row, c)) return at(row, c);
|
||||||
j = i + cols;
|
}
|
||||||
const lastRow = Math.floor((n - 1) / cols);
|
return null;
|
||||||
if (j >= n && Math.floor(i / cols) < lastRow) j = n - 1;
|
|
||||||
}
|
}
|
||||||
return j >= 0 && j < n && j !== i ? ids[j] : null;
|
if (direction !== 'up' && direction !== 'down') return null;
|
||||||
|
const step = direction === 'up' ? -1 : 1;
|
||||||
|
for (let r = row + step; r >= 0 && r < rows; r += step) {
|
||||||
|
let best = null;
|
||||||
|
let bestDistance = Infinity;
|
||||||
|
for (let c = 0; c < cols; c++) {
|
||||||
|
const id = at(r, c);
|
||||||
|
if (id && Math.abs(c - col) < bestDistance) {
|
||||||
|
best = id;
|
||||||
|
bestDistance = Math.abs(c - col);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (best) return best;
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The cell next to cell `index` in that direction (Move Tile: a tile moves
|
||||||
|
* into an empty neighbour cell, or swaps with a tiled one), or -1 at the
|
||||||
|
* edge. Adjacent only: a move never jumps over a cell.
|
||||||
|
*
|
||||||
|
* @param {number} index - the cell, in reading order
|
||||||
|
* @param {'left'|'right'|'up'|'down'} direction
|
||||||
|
* @param {number} cols - the layout's column count
|
||||||
|
* @param {number} cellCount - cols x rows
|
||||||
|
* @returns {number}
|
||||||
|
*/
|
||||||
|
function tileCellInDirection(index, direction, cols, cellCount) {
|
||||||
|
if (!(cols >= 1) || index < 0 || index >= cellCount) return -1;
|
||||||
|
const col = index % cols;
|
||||||
|
let j = -1;
|
||||||
|
if (direction === 'left') j = col > 0 ? index - 1 : -1;
|
||||||
|
else if (direction === 'right') j = col < cols - 1 ? index + 1 : -1;
|
||||||
|
else if (direction === 'up') j = index - cols;
|
||||||
|
else if (direction === 'down') j = index + cols;
|
||||||
|
return j >= 0 && j < cellCount ? j : -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The grid's cells after its shape changed (or to fill one for the first
|
||||||
|
* time): `cols` x `rows` cells, `null` for an empty one. The same shape keeps
|
||||||
|
* every cell as it is. A new shape keeps each tile at its row and column when
|
||||||
|
* every tile still fits there (2x2 growing to 3x2: the four tiles stay put),
|
||||||
|
* and otherwise packs the tiles in reading order from the first cell, holes
|
||||||
|
* collapsed (positions do not map between shapes). `oldCols` 0 (no layout
|
||||||
|
* yet) always packs.
|
||||||
|
*
|
||||||
|
* @param {(string|null)[]} cells - the current cells, laid out `oldCols` wide
|
||||||
|
* @param {number} oldCols - the column count they were laid out with
|
||||||
|
* @param {number} cols
|
||||||
|
* @param {number} rows
|
||||||
|
* @returns {(string|null)[]}
|
||||||
|
*/
|
||||||
|
function fitTileCells(cells, oldCols, cols, rows) {
|
||||||
|
const size = Math.max(0, cols * rows);
|
||||||
|
if (oldCols === cols && cells.length === size) return cells.slice();
|
||||||
|
const out = new Array(size).fill(null);
|
||||||
|
const placed = cells.map((id, k) => (id ? { id, row: Math.floor(k / oldCols), col: k % oldCols } : null));
|
||||||
|
const keep = oldCols >= 1 && placed.every((p) => !p || (p.row < rows && p.col < cols));
|
||||||
|
if (keep) {
|
||||||
|
for (const p of placed) if (p) out[p.row * cols + p.col] = p.id;
|
||||||
|
return out;
|
||||||
|
}
|
||||||
|
cells.filter(Boolean).slice(0, size).forEach((id, k) => {
|
||||||
|
out[k] = id;
|
||||||
|
});
|
||||||
|
return out;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -2204,6 +2284,8 @@ if (typeof window !== 'undefined') {
|
|||||||
dragTrackFractions,
|
dragTrackFractions,
|
||||||
tileNeighbor,
|
tileNeighbor,
|
||||||
tileInDirection,
|
tileInDirection,
|
||||||
|
tileCellInDirection,
|
||||||
|
fitTileCells,
|
||||||
cycleTile,
|
cycleTile,
|
||||||
tileGridOpenSet,
|
tileGridOpenSet,
|
||||||
TILE_GRID_MAX,
|
TILE_GRID_MAX,
|
||||||
|
|||||||
@@ -6,9 +6,17 @@
|
|||||||
* cell clears the minimum tile size.
|
* cell clears the minimum tile size.
|
||||||
* - `tileGridCapacity`: how many tiles a grid area can hold.
|
* - `tileGridCapacity`: how many tiles a grid area can hold.
|
||||||
* - `sanitizeTileGridState`: the stored `codeman:tile-grid` value made safe to
|
* - `sanitizeTileGridState`: the stored `codeman:tile-grid` value made safe to
|
||||||
* apply (unknown, deleted, detached and duplicate ids dropped).
|
* apply (unknown, deleted, detached and duplicate ids dropped); the stored
|
||||||
|
* `ids` are the grid's cells, `null` for a hole, and come back as `cells`
|
||||||
|
* (holes kept, a dropped id a hole) beside the packed `ids`. The old packed
|
||||||
|
* format reads as cells with no hole.
|
||||||
|
* - `fitTileCells`: the cells after a shape change (owner: an empty cell can be
|
||||||
|
* any cell). Same shape: unchanged; a new one: each tile keeps its row and
|
||||||
|
* column when all fit, else the tiles pack in reading order.
|
||||||
* - `tileNeighbor`, `tileInDirection`, `cycleTile`: which tile takes focus when
|
* - `tileNeighbor`, `tileInDirection`, `cycleTile`: which tile takes focus when
|
||||||
* one leaves, on a directional chord, and on Ctrl+Tab / Alt+[ ].
|
* one leaves, on a directional chord (over cells: never onto a hole), and on
|
||||||
|
* Ctrl+Tab / Alt+[ ]. `tileCellInDirection`: the adjacent cell a Move Tile
|
||||||
|
* chord moves into or swaps with.
|
||||||
*
|
*
|
||||||
* Loaded via `vm` like split-pane-helpers.test.ts. Port: N/A.
|
* Loaded via `vm` like split-pane-helpers.test.ts. Port: N/A.
|
||||||
*/
|
*/
|
||||||
@@ -23,7 +31,9 @@ type TileGrid = {
|
|||||||
tileGridCapacity(p: Record<string, number>): number;
|
tileGridCapacity(p: Record<string, number>): number;
|
||||||
sanitizeTileGridState(raw: unknown, live: unknown, detached?: Set<string>): Record<string, unknown> | null;
|
sanitizeTileGridState(raw: unknown, live: unknown, detached?: Set<string>): Record<string, unknown> | null;
|
||||||
tileNeighbor(ids: string[], id: string): string | null;
|
tileNeighbor(ids: string[], id: string): string | null;
|
||||||
tileInDirection(ids: string[], focused: string, dir: string, cols: number): string | null;
|
tileInDirection(cells: Array<string | null>, focused: string, dir: string, cols: number): string | null;
|
||||||
|
tileCellInDirection(index: number, dir: string, cols: number, cellCount: number): number;
|
||||||
|
fitTileCells(cells: Array<string | null>, oldCols: number, cols: number, rows: number): Array<string | null>;
|
||||||
cycleTile(ids: string[], focused: string, delta: number): string | null;
|
cycleTile(ids: string[], focused: string, delta: number): string | null;
|
||||||
TILE_GRID_MAX: number;
|
TILE_GRID_MAX: number;
|
||||||
TILE_LAYOUT_MAX: number;
|
TILE_LAYOUT_MAX: number;
|
||||||
@@ -111,6 +121,7 @@ describe('sanitizeTileGridState', () => {
|
|||||||
v: 1,
|
v: 1,
|
||||||
open: true,
|
open: true,
|
||||||
ids: ['a', 'b'],
|
ids: ['a', 'b'],
|
||||||
|
cells: ['a', 'b'],
|
||||||
focused: 'b',
|
focused: 'b',
|
||||||
zoomed: 'a',
|
zoomed: 'a',
|
||||||
colFr: [1, 2],
|
colFr: [1, 2],
|
||||||
@@ -161,6 +172,33 @@ describe('sanitizeTileGridState', () => {
|
|||||||
expect(out?.rowFr).toBeNull();
|
expect(out?.rowFr).toBeNull();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('stored cells keep their holes; the list beside them is packed', () => {
|
||||||
|
const raw = { v: 1, open: true, ids: ['a', null, 'b', 'c', 'd', null], focused: 'c' };
|
||||||
|
const out = T.sanitizeTileGridState(raw, live);
|
||||||
|
expect(out?.cells).toEqual(['a', null, 'b', 'c', 'd', null]);
|
||||||
|
expect(out?.ids).toEqual(['a', 'b', 'c', 'd']);
|
||||||
|
expect(out?.focused).toBe('c');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('a dropped id (gone, detached, a duplicate, malformed) leaves a hole where it was, never a shift', () => {
|
||||||
|
const raw = { v: 1, open: true, ids: ['a', 'gone', 'b', 'a', 7, 'c'] };
|
||||||
|
const out = T.sanitizeTileGridState(raw, live, new Set(['b']));
|
||||||
|
expect(out?.cells).toEqual(['a', null, null, null, null, 'c']);
|
||||||
|
expect(out?.ids).toEqual(['a', 'c']);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('the old packed format reads unchanged: cells with no hole', () => {
|
||||||
|
const out = T.sanitizeTileGridState({ v: 1, open: true, ids: ['a', 'b', 'c'] }, live);
|
||||||
|
expect(out?.cells).toEqual(['a', 'b', 'c']);
|
||||||
|
expect(out?.ids).toEqual(['a', 'b', 'c']);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('past the cap a tile becomes a hole; no more cells than the largest layout', () => {
|
||||||
|
const many = Array.from({ length: 12 }, (_, i) => `s${i}`);
|
||||||
|
const out = T.sanitizeTileGridState({ v: 1, open: true, ids: many }, many);
|
||||||
|
expect(out?.cells).toEqual([...many.slice(0, 6), null, null, null]);
|
||||||
|
});
|
||||||
|
|
||||||
it.each([null, 'not json', '[]', 42, { v: 2, ids: ['a'] }, { ids: ['a'] }])('rejects %j', (raw) => {
|
it.each([null, 'not json', '[]', 42, { v: 2, ids: ['a'] }, { ids: ['a'] }])('rejects %j', (raw) => {
|
||||||
expect(T.sanitizeTileGridState(raw, live)).toBeNull();
|
expect(T.sanitizeTileGridState(raw, live)).toBeNull();
|
||||||
});
|
});
|
||||||
@@ -190,6 +228,47 @@ describe('focus helpers', () => {
|
|||||||
expect(T.tileInDirection(ids, 'e', 'down', 3)).toBeNull();
|
expect(T.tileInDirection(ids, 'e', 'down', 3)).toBeNull();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('tileInDirection never lands on a hole: along a row it skips one, never leaving the row', () => {
|
||||||
|
// 3x2: a _ c
|
||||||
|
// d e f
|
||||||
|
const cells = ['a', null, 'c', 'd', 'e', 'f'];
|
||||||
|
expect(T.tileInDirection(cells, 'a', 'right', 3)).toBe('c');
|
||||||
|
expect(T.tileInDirection(cells, 'c', 'left', 3)).toBe('a');
|
||||||
|
expect(T.tileInDirection(cells, 'c', 'right', 3)).toBeNull();
|
||||||
|
expect(T.tileInDirection(cells, 'd', 'left', 3)).toBeNull();
|
||||||
|
// Up from e (a hole above): the nearest tile in that row, the lower column on a tie.
|
||||||
|
expect(T.tileInDirection(cells, 'e', 'up', 3)).toBe('a');
|
||||||
|
expect(T.tileInDirection(cells, 'f', 'up', 3)).toBe('c');
|
||||||
|
expect(T.tileInDirection(cells, 'a', 'down', 3)).toBe('d');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('tileInDirection: up and down go to the nearest row that has a tile, even across a column', () => {
|
||||||
|
// 3x2: a b c
|
||||||
|
// _ _ f
|
||||||
|
const cells = ['a', 'b', 'c', null, null, 'f'];
|
||||||
|
expect(T.tileInDirection(cells, 'a', 'down', 3)).toBe('f');
|
||||||
|
expect(T.tileInDirection(cells, 'c', 'down', 3)).toBe('f');
|
||||||
|
expect(T.tileInDirection(cells, 'f', 'up', 3)).toBe('c');
|
||||||
|
expect(T.tileInDirection(cells, 'f', 'left', 3)).toBeNull();
|
||||||
|
// A row with no tile at all is passed over (a 3x3, which the layout table keeps).
|
||||||
|
const tall = ['a', null, null, null, null, null, null, 'h', null];
|
||||||
|
expect(T.tileInDirection(tall, 'a', 'down', 3)).toBe('h');
|
||||||
|
expect(T.tileInDirection(tall, 'h', 'up', 3)).toBe('a');
|
||||||
|
// A hole is never the focused cell either.
|
||||||
|
expect(T.tileInDirection(cells, null as unknown as string, 'up', 3)).toBeNull();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('tileCellInDirection: the adjacent cell, or -1 at the edge', () => {
|
||||||
|
// 3x2 cells 0 1 2 / 3 4 5
|
||||||
|
expect([0, 1, 2, 3, 4, 5].map((i) => T.tileCellInDirection(i, 'left', 3, 6))).toEqual([-1, 0, 1, -1, 3, 4]);
|
||||||
|
expect([0, 1, 2, 3, 4, 5].map((i) => T.tileCellInDirection(i, 'right', 3, 6))).toEqual([1, 2, -1, 4, 5, -1]);
|
||||||
|
expect([0, 1, 2, 3, 4, 5].map((i) => T.tileCellInDirection(i, 'up', 3, 6))).toEqual([-1, -1, -1, 0, 1, 2]);
|
||||||
|
expect([0, 1, 2, 3, 4, 5].map((i) => T.tileCellInDirection(i, 'down', 3, 6))).toEqual([3, 4, 5, -1, -1, -1]);
|
||||||
|
// 2x2
|
||||||
|
expect([0, 1, 2, 3].map((i) => T.tileCellInDirection(i, 'down', 2, 4))).toEqual([2, 3, -1, -1]);
|
||||||
|
expect(T.tileCellInDirection(7, 'left', 3, 6)).toBe(-1);
|
||||||
|
});
|
||||||
|
|
||||||
it('cycleTile wraps in reading order', () => {
|
it('cycleTile wraps in reading order', () => {
|
||||||
expect(T.cycleTile(['a', 'b', 'c'], 'c', 1)).toBe('a');
|
expect(T.cycleTile(['a', 'b', 'c'], 'c', 1)).toBe('a');
|
||||||
expect(T.cycleTile(['a', 'b', 'c'], 'a', -1)).toBe('c');
|
expect(T.cycleTile(['a', 'b', 'c'], 'a', -1)).toBe('c');
|
||||||
@@ -197,6 +276,32 @@ describe('focus helpers', () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe('fitTileCells (the cells after a shape change)', () => {
|
||||||
|
it('the same shape keeps every cell, holes included', () => {
|
||||||
|
expect(T.fitTileCells(['a', null, 'c', 'd', 'e', 'f'], 3, 3, 2)).toEqual(['a', null, 'c', 'd', 'e', 'f']);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('a first layout (no columns yet) packs and pads', () => {
|
||||||
|
expect(T.fitTileCells(['a', 'b', 'c', 'd', 'e'], 0, 3, 2)).toEqual(['a', 'b', 'c', 'd', 'e', null]);
|
||||||
|
expect(T.fitTileCells(['a', 'b', 'c'], 0, 2, 2)).toEqual(['a', 'b', 'c', null]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('growing 2x2 to 3x2: every tile keeps its row and column', () => {
|
||||||
|
expect(T.fitTileCells(['a', 'b', 'c', 'd'], 2, 3, 2)).toEqual(['a', 'b', null, 'c', 'd', null]);
|
||||||
|
// 2x1 to 2x2 likewise (the same as packing there).
|
||||||
|
expect(T.fitTileCells(['a', 'b'], 2, 2, 2)).toEqual(['a', 'b', null, null]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('shrinking: kept where every tile fits, else packed in reading order', () => {
|
||||||
|
// 3x2 -> 2x2 with the third column empty: the tiles stay put.
|
||||||
|
expect(T.fitTileCells(['a', 'b', null, 'd', 'e', null], 3, 2, 2)).toEqual(['a', 'b', 'd', 'e']);
|
||||||
|
// A tile in the third column: packed, holes collapsed.
|
||||||
|
expect(T.fitTileCells(['a', null, 'c', 'd', 'e', null], 3, 2, 2)).toEqual(['a', 'c', 'd', 'e']);
|
||||||
|
// 2x2 -> 3x1 (a wider window, three tiles): row 1 does not fit, packed.
|
||||||
|
expect(T.fitTileCells(['a', null, 'c', 'd'], 2, 3, 1)).toEqual(['a', 'c', 'd']);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
describe('dragTrackFractions (divider drags)', () => {
|
describe('dragTrackFractions (divider drags)', () => {
|
||||||
const drag = (fr: number[], i: number, d: number, total = 1200, min = 300) =>
|
const drag = (fr: number[], i: number, d: number, total = 1200, min = 300) =>
|
||||||
(T as unknown as { dragTrackFractions: (...a: unknown[]) => number[] }).dragTrackFractions(fr, i, d, total, min);
|
(T as unknown as { dragTrackFractions: (...a: unknown[]) => number[] }).dragTrackFractions(fr, i, d, total, min);
|
||||||
|
|||||||
Reference in New Issue
Block a user