Refactor grid state tracking
This commit is contained in:
parent
978b0f08e8
commit
8d46687a54
2 changed files with 112 additions and 64 deletions
|
|
@ -17,8 +17,11 @@ limitations under the License.
|
||||||
import { SpringRef, TransitionFn, useTransition } from "@react-spring/web";
|
import { SpringRef, TransitionFn, useTransition } from "@react-spring/web";
|
||||||
import { EventTypes, Handler, useScroll } from "@use-gesture/react";
|
import { EventTypes, Handler, useScroll } from "@use-gesture/react";
|
||||||
import React, {
|
import React, {
|
||||||
|
Dispatch,
|
||||||
FC,
|
FC,
|
||||||
ReactNode,
|
ReactNode,
|
||||||
|
SetStateAction,
|
||||||
|
useCallback,
|
||||||
useEffect,
|
useEffect,
|
||||||
useMemo,
|
useMemo,
|
||||||
useRef,
|
useRef,
|
||||||
|
|
@ -39,8 +42,82 @@ import {
|
||||||
fillGaps,
|
fillGaps,
|
||||||
forEachCellInArea,
|
forEachCellInArea,
|
||||||
cycleTileSize,
|
cycleTileSize,
|
||||||
|
appendItems,
|
||||||
} from "./model";
|
} from "./model";
|
||||||
|
|
||||||
|
interface GridState extends Grid {
|
||||||
|
/**
|
||||||
|
* The ID of the current state of the grid.
|
||||||
|
*/
|
||||||
|
generation: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
const useGridState = (
|
||||||
|
columns: number | null,
|
||||||
|
items: TileDescriptor[]
|
||||||
|
): [GridState | null, Dispatch<SetStateAction<Grid>>] => {
|
||||||
|
const [grid, setGrid_] = useReactiveState<GridState | null>(
|
||||||
|
(prevGrid = null) => {
|
||||||
|
if (prevGrid === null) {
|
||||||
|
// We can't do anything if the column count isn't known yet
|
||||||
|
if (columns === null) {
|
||||||
|
return null;
|
||||||
|
} else {
|
||||||
|
prevGrid = { generation: 0, columns, cells: [] };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Step 1: Update tiles that still exist, and remove tiles that have left
|
||||||
|
// the grid
|
||||||
|
const itemsById = new Map(items.map((i) => [i.id, i]));
|
||||||
|
const grid1: Grid = {
|
||||||
|
...prevGrid,
|
||||||
|
cells: prevGrid.cells.map((c) => {
|
||||||
|
if (c === undefined) return undefined;
|
||||||
|
const item = itemsById.get(c.item.id);
|
||||||
|
return item === undefined ? undefined : { ...c, item };
|
||||||
|
}),
|
||||||
|
};
|
||||||
|
|
||||||
|
// Step 2: Backfill gaps left behind by removed tiles
|
||||||
|
const grid2 = fillGaps(grid1);
|
||||||
|
|
||||||
|
// Step 3: Add new tiles to the end of the grid
|
||||||
|
const existingItemIds = new Set(
|
||||||
|
grid2.cells.filter((c) => c !== undefined).map((c) => c!.item.id)
|
||||||
|
);
|
||||||
|
const newItems = items.filter((i) => !existingItemIds.has(i.id));
|
||||||
|
const grid3 = appendItems(newItems, grid2);
|
||||||
|
|
||||||
|
return { ...grid3, generation: prevGrid.generation + 1 };
|
||||||
|
},
|
||||||
|
[columns, items]
|
||||||
|
);
|
||||||
|
|
||||||
|
const setGrid: Dispatch<SetStateAction<Grid>> = useCallback(
|
||||||
|
(action) => {
|
||||||
|
if (typeof action === "function") {
|
||||||
|
setGrid_((prevGrid) =>
|
||||||
|
prevGrid === null
|
||||||
|
? null
|
||||||
|
: {
|
||||||
|
...(action as (prev: Grid) => Grid)(prevGrid),
|
||||||
|
generation: prevGrid.generation + 1,
|
||||||
|
}
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
setGrid_((prevGrid) => ({
|
||||||
|
...action,
|
||||||
|
generation: prevGrid?.generation ?? 1,
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
},
|
||||||
|
[setGrid_]
|
||||||
|
);
|
||||||
|
|
||||||
|
return [grid, setGrid];
|
||||||
|
};
|
||||||
|
|
||||||
interface Rect {
|
interface Rect {
|
||||||
x: number;
|
x: number;
|
||||||
y: number;
|
y: number;
|
||||||
|
|
@ -133,55 +210,7 @@ export const NewVideoGrid: FC<Props> = ({
|
||||||
[gridBounds]
|
[gridBounds]
|
||||||
);
|
);
|
||||||
|
|
||||||
const [grid, setGrid] = useReactiveState<Grid | null>(
|
const [grid, setGrid] = useGridState(columns, items);
|
||||||
(prevGrid = null) => {
|
|
||||||
if (prevGrid === null) {
|
|
||||||
// We can't do anything if the column count isn't known yet
|
|
||||||
if (columns === null) {
|
|
||||||
return null;
|
|
||||||
} else {
|
|
||||||
prevGrid = { generation: slotGridGeneration, columns, cells: [] };
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Step 1: Update tiles that still exist, and remove tiles that have left
|
|
||||||
// the grid
|
|
||||||
const itemsById = new Map(items.map((i) => [i.id, i]));
|
|
||||||
const grid1: Grid = {
|
|
||||||
...prevGrid,
|
|
||||||
generation: prevGrid.generation + 1,
|
|
||||||
cells: prevGrid.cells.map((c) => {
|
|
||||||
if (c === undefined) return undefined;
|
|
||||||
const item = itemsById.get(c.item.id);
|
|
||||||
return item === undefined ? undefined : { ...c, item };
|
|
||||||
}),
|
|
||||||
};
|
|
||||||
|
|
||||||
// Step 2: Backfill gaps left behind by removed tiles
|
|
||||||
const grid2 = fillGaps(grid1);
|
|
||||||
|
|
||||||
// Step 3: Add new tiles to the end of the grid
|
|
||||||
const existingItemIds = new Set(
|
|
||||||
grid2.cells.filter((c) => c !== undefined).map((c) => c!.item.id)
|
|
||||||
);
|
|
||||||
const newItems = items.filter((i) => !existingItemIds.has(i.id));
|
|
||||||
const grid3: Grid = {
|
|
||||||
...grid2,
|
|
||||||
cells: [
|
|
||||||
...grid2.cells,
|
|
||||||
...newItems.map((i) => ({
|
|
||||||
item: i,
|
|
||||||
slot: true,
|
|
||||||
columns: 1,
|
|
||||||
rows: 1,
|
|
||||||
})),
|
|
||||||
],
|
|
||||||
};
|
|
||||||
|
|
||||||
return grid3;
|
|
||||||
},
|
|
||||||
[items, columns]
|
|
||||||
);
|
|
||||||
|
|
||||||
const [tiles] = useReactiveState<Tile[]>(
|
const [tiles] = useReactiveState<Tile[]>(
|
||||||
(prevTiles) => {
|
(prevTiles) => {
|
||||||
|
|
@ -189,9 +218,9 @@ export const NewVideoGrid: FC<Props> = ({
|
||||||
// the update, because grid and slotRects will be out of sync
|
// the update, because grid and slotRects will be out of sync
|
||||||
if (slotGridGeneration !== grid?.generation) return prevTiles ?? [];
|
if (slotGridGeneration !== grid?.generation) return prevTiles ?? [];
|
||||||
|
|
||||||
const slotCells = grid.cells.filter((c) => c?.slot) as Cell[];
|
const tileCells = grid.cells.filter((c) => c?.origin) as Cell[];
|
||||||
const tileRects = new Map<TileDescriptor, Rect>(
|
const tileRects = new Map<TileDescriptor, Rect>(
|
||||||
zipWith(slotCells, slotRects, (cell, rect) => [cell.item, rect])
|
zipWith(tileCells, slotRects, (cell, rect) => [cell.item, rect])
|
||||||
);
|
);
|
||||||
return items.map((item) => ({ ...tileRects.get(item)!, item }));
|
return items.map((item) => ({ ...tileRects.get(item)!, item }));
|
||||||
},
|
},
|
||||||
|
|
@ -247,7 +276,7 @@ export const NewVideoGrid: FC<Props> = ({
|
||||||
let slotId = 0;
|
let slotId = 0;
|
||||||
for (let i = 0; i < grid.cells.length; i++) {
|
for (let i = 0; i < grid.cells.length; i++) {
|
||||||
const cell = grid.cells[i];
|
const cell = grid.cells[i];
|
||||||
if (cell?.slot) {
|
if (cell?.origin) {
|
||||||
const slotEnd = i + cell.columns - 1 + grid.columns * (cell.rows - 1);
|
const slotEnd = i + cell.columns - 1 + grid.columns * (cell.rows - 1);
|
||||||
forEachCellInArea(
|
forEachCellInArea(
|
||||||
i,
|
i,
|
||||||
|
|
|
||||||
|
|
@ -17,29 +17,34 @@ limitations under the License.
|
||||||
import TinyQueue from "tinyqueue";
|
import TinyQueue from "tinyqueue";
|
||||||
import { TileDescriptor } from "./TileDescriptor";
|
import { TileDescriptor } from "./TileDescriptor";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A 1×1 cell in a grid which belongs to a tile.
|
||||||
|
*/
|
||||||
export interface Cell {
|
export interface Cell {
|
||||||
/**
|
/**
|
||||||
* The item held by the slot containing this cell.
|
* The item displayed on the tile.
|
||||||
*/
|
*/
|
||||||
item: TileDescriptor;
|
item: TileDescriptor;
|
||||||
/**
|
/**
|
||||||
* Whether this cell is the first cell of the containing slot.
|
* Whether this cell is the origin (top left corner) of the tile.
|
||||||
*/
|
*/
|
||||||
// TODO: Rename to 'start'?
|
origin: boolean;
|
||||||
slot: boolean;
|
|
||||||
/**
|
/**
|
||||||
* The width, in columns, of the containing slot.
|
* The width, in columns, of the tile.
|
||||||
*/
|
*/
|
||||||
columns: number;
|
columns: number;
|
||||||
/**
|
/**
|
||||||
* The height, in rows, of the containing slot.
|
* The height, in rows, of the tile.
|
||||||
*/
|
*/
|
||||||
rows: number;
|
rows: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface Grid {
|
export interface Grid {
|
||||||
generation: number;
|
|
||||||
columns: number;
|
columns: number;
|
||||||
|
/**
|
||||||
|
* The cells of the grid, in left-to-right top-to-bottom order.
|
||||||
|
* undefined = empty.
|
||||||
|
*/
|
||||||
cells: (Cell | undefined)[];
|
cells: (Cell | undefined)[];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -55,9 +60,9 @@ export function dijkstra(g: Grid): number[] {
|
||||||
|
|
||||||
const visit = (curr: number, via: number) => {
|
const visit = (curr: number, via: number) => {
|
||||||
const viaCell = g.cells[via];
|
const viaCell = g.cells[via];
|
||||||
const viaLargeSlot =
|
const viaLargeTile =
|
||||||
viaCell !== undefined && (viaCell.rows > 1 || viaCell.columns > 1);
|
viaCell !== undefined && (viaCell.rows > 1 || viaCell.columns > 1);
|
||||||
const distanceVia = distances[via] + (viaLargeSlot ? 4 : 1);
|
const distanceVia = distances[via] + (viaLargeTile ? 4 : 1);
|
||||||
|
|
||||||
if (distanceVia < distances[curr]) {
|
if (distanceVia < distances[curr]) {
|
||||||
distances[curr] = distanceVia;
|
distances[curr] = distanceVia;
|
||||||
|
|
@ -252,6 +257,21 @@ export function fillGaps(g: Grid): Grid {
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function appendItems(items: TileDescriptor[], g: Grid): Grid {
|
||||||
|
return {
|
||||||
|
...g,
|
||||||
|
cells: [
|
||||||
|
...g.cells,
|
||||||
|
...items.map((i) => ({
|
||||||
|
item: i,
|
||||||
|
origin: true,
|
||||||
|
columns: 1,
|
||||||
|
rows: 1,
|
||||||
|
})),
|
||||||
|
],
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
export function cycleTileSize(tileId: string, g: Grid): Grid {
|
export function cycleTileSize(tileId: string, g: Grid): Grid {
|
||||||
const from = g.cells.findIndex((c) => c?.item.id === tileId);
|
const from = g.cells.findIndex((c) => c?.item.id === tileId);
|
||||||
if (from === -1) return g; // Tile removed, no change
|
if (from === -1) return g; // Tile removed, no change
|
||||||
|
|
@ -273,7 +293,6 @@ export function cycleTileSize(tileId: string, g: Grid): Grid {
|
||||||
|
|
||||||
const gappyGrid: Grid = {
|
const gappyGrid: Grid = {
|
||||||
...g,
|
...g,
|
||||||
generation: g.generation + 1,
|
|
||||||
cells: new Array(g.cells.length + newRows * g.columns),
|
cells: new Array(g.cells.length + newRows * g.columns),
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
@ -318,7 +337,7 @@ export function cycleTileSize(tileId: string, g: Grid): Grid {
|
||||||
const toRow = row(to, g);
|
const toRow = row(to, g);
|
||||||
|
|
||||||
g.cells.forEach((c, src) => {
|
g.cells.forEach((c, src) => {
|
||||||
if (c?.slot && c.item.id !== tileId) {
|
if (c?.origin && c.item.id !== tileId) {
|
||||||
const offset =
|
const offset =
|
||||||
row(src, g) > toRow + candidateHeight - 1 ? g.columns * newRows : 0;
|
row(src, g) > toRow + candidateHeight - 1 ? g.columns * newRows : 0;
|
||||||
forEachCellInArea(src, areaEnd(src, c.columns, c.rows, g), g, (c, i) => {
|
forEachCellInArea(src, areaEnd(src, c.columns, c.rows, g), g, (c, i) => {
|
||||||
|
|
@ -333,7 +352,7 @@ export function cycleTileSize(tileId: string, g: Grid): Grid {
|
||||||
if (c !== undefined) displacedTiles.push(c);
|
if (c !== undefined) displacedTiles.push(c);
|
||||||
gappyGrid.cells[i] = {
|
gappyGrid.cells[i] = {
|
||||||
item: g.cells[from]!.item,
|
item: g.cells[from]!.item,
|
||||||
slot: i === to,
|
origin: i === to,
|
||||||
columns: toWidth,
|
columns: toWidth,
|
||||||
rows: toHeight,
|
rows: toHeight,
|
||||||
};
|
};
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue