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 { EventTypes, Handler, useScroll } from "@use-gesture/react";
 | 
			
		||||
import React, {
 | 
			
		||||
  Dispatch,
 | 
			
		||||
  FC,
 | 
			
		||||
  ReactNode,
 | 
			
		||||
  SetStateAction,
 | 
			
		||||
  useCallback,
 | 
			
		||||
  useEffect,
 | 
			
		||||
  useMemo,
 | 
			
		||||
  useRef,
 | 
			
		||||
| 
						 | 
				
			
			@ -39,8 +42,82 @@ import {
 | 
			
		|||
  fillGaps,
 | 
			
		||||
  forEachCellInArea,
 | 
			
		||||
  cycleTileSize,
 | 
			
		||||
  appendItems,
 | 
			
		||||
} 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 {
 | 
			
		||||
  x: number;
 | 
			
		||||
  y: number;
 | 
			
		||||
| 
						 | 
				
			
			@ -133,55 +210,7 @@ export const NewVideoGrid: FC<Props> = ({
 | 
			
		|||
    [gridBounds]
 | 
			
		||||
  );
 | 
			
		||||
 | 
			
		||||
  const [grid, setGrid] = useReactiveState<Grid | 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: 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 [grid, setGrid] = useGridState(columns, items);
 | 
			
		||||
 | 
			
		||||
  const [tiles] = useReactiveState<Tile[]>(
 | 
			
		||||
    (prevTiles) => {
 | 
			
		||||
| 
						 | 
				
			
			@ -189,9 +218,9 @@ export const NewVideoGrid: FC<Props> = ({
 | 
			
		|||
      // the update, because grid and slotRects will be out of sync
 | 
			
		||||
      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>(
 | 
			
		||||
        zipWith(slotCells, slotRects, (cell, rect) => [cell.item, rect])
 | 
			
		||||
        zipWith(tileCells, slotRects, (cell, rect) => [cell.item, rect])
 | 
			
		||||
      );
 | 
			
		||||
      return items.map((item) => ({ ...tileRects.get(item)!, item }));
 | 
			
		||||
    },
 | 
			
		||||
| 
						 | 
				
			
			@ -247,7 +276,7 @@ export const NewVideoGrid: FC<Props> = ({
 | 
			
		|||
    let slotId = 0;
 | 
			
		||||
    for (let i = 0; i < grid.cells.length; i++) {
 | 
			
		||||
      const cell = grid.cells[i];
 | 
			
		||||
      if (cell?.slot) {
 | 
			
		||||
      if (cell?.origin) {
 | 
			
		||||
        const slotEnd = i + cell.columns - 1 + grid.columns * (cell.rows - 1);
 | 
			
		||||
        forEachCellInArea(
 | 
			
		||||
          i,
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -17,29 +17,34 @@ limitations under the License.
 | 
			
		|||
import TinyQueue from "tinyqueue";
 | 
			
		||||
import { TileDescriptor } from "./TileDescriptor";
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * A 1×1 cell in a grid which belongs to a tile.
 | 
			
		||||
 */
 | 
			
		||||
export interface Cell {
 | 
			
		||||
  /**
 | 
			
		||||
   * The item held by the slot containing this cell.
 | 
			
		||||
   * The item displayed on the tile.
 | 
			
		||||
   */
 | 
			
		||||
  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'?
 | 
			
		||||
  slot: boolean;
 | 
			
		||||
  origin: boolean;
 | 
			
		||||
  /**
 | 
			
		||||
   * The width, in columns, of the containing slot.
 | 
			
		||||
   * The width, in columns, of the tile.
 | 
			
		||||
   */
 | 
			
		||||
  columns: number;
 | 
			
		||||
  /**
 | 
			
		||||
   * The height, in rows, of the containing slot.
 | 
			
		||||
   * The height, in rows, of the tile.
 | 
			
		||||
   */
 | 
			
		||||
  rows: number;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export interface Grid {
 | 
			
		||||
  generation: number;
 | 
			
		||||
  columns: number;
 | 
			
		||||
  /**
 | 
			
		||||
   * The cells of the grid, in left-to-right top-to-bottom order.
 | 
			
		||||
   * undefined = empty.
 | 
			
		||||
   */
 | 
			
		||||
  cells: (Cell | undefined)[];
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -55,9 +60,9 @@ export function dijkstra(g: Grid): number[] {
 | 
			
		|||
 | 
			
		||||
  const visit = (curr: number, via: number) => {
 | 
			
		||||
    const viaCell = g.cells[via];
 | 
			
		||||
    const viaLargeSlot =
 | 
			
		||||
    const viaLargeTile =
 | 
			
		||||
      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]) {
 | 
			
		||||
      distances[curr] = distanceVia;
 | 
			
		||||
| 
						 | 
				
			
			@ -252,6 +257,21 @@ export function fillGaps(g: Grid): Grid {
 | 
			
		|||
  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 {
 | 
			
		||||
  const from = g.cells.findIndex((c) => c?.item.id === tileId);
 | 
			
		||||
  if (from === -1) return g; // Tile removed, no change
 | 
			
		||||
| 
						 | 
				
			
			@ -273,7 +293,6 @@ export function cycleTileSize(tileId: string, g: Grid): Grid {
 | 
			
		|||
 | 
			
		||||
  const gappyGrid: Grid = {
 | 
			
		||||
    ...g,
 | 
			
		||||
    generation: g.generation + 1,
 | 
			
		||||
    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);
 | 
			
		||||
 | 
			
		||||
  g.cells.forEach((c, src) => {
 | 
			
		||||
    if (c?.slot && c.item.id !== tileId) {
 | 
			
		||||
    if (c?.origin && c.item.id !== tileId) {
 | 
			
		||||
      const offset =
 | 
			
		||||
        row(src, g) > toRow + candidateHeight - 1 ? g.columns * newRows : 0;
 | 
			
		||||
      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);
 | 
			
		||||
    gappyGrid.cells[i] = {
 | 
			
		||||
      item: g.cells[from]!.item,
 | 
			
		||||
      slot: i === to,
 | 
			
		||||
      origin: i === to,
 | 
			
		||||
      columns: toWidth,
 | 
			
		||||
      rows: toHeight,
 | 
			
		||||
    };
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
		Loading…
	
	Add table
		Add a link
		
	
		Reference in a new issue